#! /usr/bin/python3
# -*- coding: UTF-8 -*-

"""Small utility to download and update the configuration files of the DHCP
server installed on a XiVO, so that any phone that is supported by one
of the xivo-* provd plugins is able to boot correctly using the DHCP server
installed on a XiVO.

"""

__license__ = """
    Copyright (C) 2011  Avencall

    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.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA..
"""

import configparser
import contextlib
import optparse
import os
import sys
import tarfile
import tempfile
import urllib.error
import urllib.parse
import urllib.request

PKG_FILENAME = 'dhcpd.tar.bz2'
DHCPD_UPDATE_FILENAME = 'dhcpd_update.conf'


def _build_opener(proxies):
    return urllib.request.build_opener(urllib.request.ProxyHandler(proxies))


def _extract_pkg_file(fobj, dhcpd_dir):
    with contextlib.closing(tarfile.open(fileobj=fobj)) as tf:
        tf.extractall(dhcpd_dir)


def download(url, dhcpd_dir, proxies=None):
    # Download the package at url and extract its content into dhcpd_dir.
    opener = _build_opener(proxies)
    with tempfile.TemporaryFile() as fobj:
        with contextlib.closing(opener.open(url)) as url_fobj:
            fobj.write(url_fobj.read())
        fobj.seek(0)
        _extract_pkg_file(fobj, dhcpd_dir)


def regenerate(dhcpd_dir, subnet_file, ignore_missing):
    # Regenerate DHCP server configuration files.
    file = os.path.join(dhcpd_dir, subnet_file)
    with open(file, 'w') as fobj:
        def _concat_file(suffix):
            cur_file = os.path.join(dhcpd_dir, subnet_file + suffix)
            with open(cur_file) as cur_fobj:
                fobj.write(cur_fobj.read())

        fobj.write('# This file has been automatically generated by dhcpd-update.\n')
        _concat_file('.head')
        try:
            _concat_file('.middle')
        except EnvironmentError:
            if not ignore_missing:
                raise
        _concat_file('.tail')


def new_empty_dhcpd_update_file(dhcpd_dir):
    file = os.path.join(dhcpd_dir, DHCPD_UPDATE_FILENAME)
    try:
        fd = os.open(file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 644)
    except EnvironmentError:
        pass
    else:
        os.close(fd)


def _read_config_from_default(config):
    return {'general':
                {'config_file': '/etc/xivo/dhcpd-update.conf',
                 'update_url': 'http://provd.xivo.solutions/xivo/dhcpd-update/13.17/',
                 'dhcpd_dir': '/etc/dhcp/',
                 'subnet_file': 'dhcpd_subnet.conf'},
            }


def _read_config_from_commandline(config):
    parser = optparse.OptionParser()
    parser.add_option('-n', '--newempty', action='store_true')
    parser.add_option('-d', '--download', action='store_true')
    parser.add_option('-r', '--regenerate', action='store_true')
    parser.add_option('-i', '--ignoremissing', action='store_true')
    parser.add_option('-F', '--configfile', action='store')

    opts, args = parser.parse_args()
    result = {'general': {}}
    if opts.newempty:
        result['general']['new_empty'] = 'yes'
    if opts.download:
        result['general']['download'] = 'yes'
    if opts.regenerate:
        result['general']['regenerate'] = 'yes'
    if opts.ignoremissing:
        result['general']['ignore_missing'] = 'yes'
    if opts.configfile:
        result['general']['config_file'] = opts.configfile
    return result


def _read_config_from_file(config):
    # read config file
    config_file = config['general']['config_file']
    config_parser = configparser.RawConfigParser()
    with open(config_file) as fobj:
        config_parser.read_file(fobj)
    # create config dictionary out of it
    result = {}
    for section in config_parser.sections():
        result[section] = dict(config_parser.items(section))
    return result


def _update_config(config, new_config):
    for key in new_config:
        if key in config:
            config[key].update(new_config[key])
        else:
            config[key] = new_config[key]


def read_config():
    # Read and return the configuration for this program.
    # Config is a dictionary of dictionary, with the following keys:
    #   general
    #     config_file -- path to this application configuration file
    #     update_url
    #     dhcpd_dir
    #     subnet_file
    #     ignore_missing
    #     new_empty -- 'yes' if must create empty dhcpd update file
    #     download -- 'yes' if must download, else won't download
    #     regenerate -- 'yes' if must regenerate, else won't regenerate
    #   proxy
    #     http -- proxy for http
    config = {'general': {}, 'proxy': {}}
    _update_config(config, _read_config_from_default(config))
    cli_config = _read_config_from_commandline(config)
    _update_config(config, cli_config)
    _update_config(config, _read_config_from_file(config))
    _update_config(config, cli_config)
    return config


def _do_new_empty(config):
    dhcpd_dir = config['general']['dhcpd_dir']
    new_empty_dhcpd_update_file(dhcpd_dir)


def _do_download(config):
    url = urllib.parse.urljoin(config['general']['update_url'], PKG_FILENAME)
    dhcpd_dir = config['general']['dhcpd_dir']
    proxies = config['proxy']
    download(url, dhcpd_dir, proxies)


def _do_regenerate(config):
    dhcpd_dir = config['general']['dhcpd_dir']
    subnet_file = config['general']['subnet_file']
    ignore_missing = config['general'].get('ignore_missing') == 'yes'
    regenerate(dhcpd_dir, subnet_file, ignore_missing)


def main():
    config = read_config()

    op_new_empty = config['general'].get('new_empty') == 'yes'
    if op_new_empty:
        _do_new_empty(config)

    op_download = config['general'].get('download') == 'yes'
    if op_download:
        _do_download(config)

    op_regenerate = config['general'].get('regenerate') == 'yes'
    if op_regenerate:
        _do_regenerate(config)

    if not (op_new_empty or op_download or op_regenerate):
        # Error: no operation specified
        print('error: no operation specified', file=sys.stderr)
        raise SystemExit(2)


if __name__ == '__main__':
    main()
