#!/usr/bin/env python-3
# -*- coding: utf-8 -*-
#
#  /usr/bin/preseed
#
#  Copyright 2014 Sam Nazarko <email@samnazarko.co.uk>
#
#  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.
#

# shell return codes
# 0 = preseeding successful
# 1 = reserved (python interpreter unhandled exception)
# 2 = reserved (python interpreter file not found)
# 3 = preseed.cfg not found
# 4 = ethernet interface not connected
# 5 = wireless network not found
# 6 = wireless network found but could not connect
# 7 = Agent failed to start

import syslog  # NOQA
import os.path
import dbus
import os
import time
import sys
import subprocess

from io import open


def getvalue(valuestring):
    splitvalue = valuestring.split(' ', 3)
    line = splitvalue[3]
    return line


def format_ip(ipstring):
    a = ipstring.split('.')
    for i, s in enumerate(a):
        a[i] = str(int(s, base=10))
    return a[0] + '.' + a[1] + '.' + a[2] + '.' + a[3]


def make_variant(string):
    return dbus.String(string, variant_level=1)


def main():
    syslog.syslog("Preseed parser starting...")
    if len(sys.argv) > 1:
        filename = sys.argv[1]
    else:
        filename = "/boot/preseed.cfg"

    if not os.path.isfile(filename):
        syslog.syslog("No preseed file found")
        sys.exit(3)

    with open(filename, 'r', encoding='utf-8') as preseed_file:
        preseed_lines = preseed_file.readlines()

    with open("/proc/cmdline") as cmdfile:
        cmdline = cmdfile.read()

    network = {
        "interface": "eth",
        "auto": "true",
        "hidden": "false"
    }
    preseed_lines = [line.strip() for line in preseed_lines]
    bus = dbus.SystemBus()
    manager = dbus.Interface(bus.get_object('net.connman', '/'), 'net.connman.Manager')

    syslog.syslog("Preseed parser initialised...\n%s" % str(preseed_lines))

    for line in preseed_lines:
        if line.startswith("d-i network/interface string"):
            syslog.syslog("Detected interface definition of " + getvalue(line))
            network["interface"] = getvalue(line)
            continue
        if line.startswith("d-i network/auto boolean"):
            syslog.syslog("DHCP is set to " + getvalue(line))
            network["auto"] = getvalue(line)
            continue
        if line.startswith("d-i network/ip string"):
            syslog.syslog("IP address is set to " + format_ip(getvalue(line)))
            network["ip"] = format_ip(getvalue(line))
            continue
        if line.startswith("d-i network/mask string"):
            syslog.syslog("Subnet mask is set to " + format_ip(getvalue(line)))
            network["mask"] = format_ip(getvalue(line))
            continue
        if line.startswith("d-i network/dns1 string"):
            syslog.syslog("DNS1 is set to " + format_ip(getvalue(line)))
            network["dns1"] = format_ip(getvalue(line))
            continue
        if line.startswith("d-i network/dns2 string"):
            syslog.syslog("DNS2 is set to " + format_ip(getvalue(line)))
            network["dns2"] = format_ip(getvalue(line))
            continue
        if line.startswith("d-i network/gw string"):
            syslog.syslog("Gateway is set to " + format_ip(getvalue(line)))
            network["gw"] = format_ip(getvalue(line))
            continue
        if line.startswith("d-i network/ssid string"):
            syslog.syslog("SSID is set to " + getvalue(line))
            network["ssid"] = getvalue(line)
            continue
        if line.startswith("d-i network/wlan_keytype string"):
            syslog.syslog("WLAN key type is set to " + getvalue(line))
            network["keytype"] = getvalue(line)
            continue
        if line.startswith("d-i network/wlan_key string"):
            wlan_key = getvalue(line)
            key_length = len(wlan_key)
            if key_length > 2:
                log_key = wlan_key[0] + (key_length - 2) * '*' + wlan_key[-1]
            else:
                log_key = wlan_key
            syslog.syslog("WLAN key is set to " + log_key)
            network["keyvalue"] = wlan_key
            continue
        if line.startswith("d-i network/hidden boolean"):
            syslog.syslog("hidden is set to " + getvalue(line))
            network["hidden"] = getvalue(line)
            continue

    syslog.syslog("Parsing completed. Performing setup...\n%s" % str(network))
    if (network["interface"] == "wlan") or (network["auto"] == "false") and "nfs" not in cmdline:
        syslog.syslog("Non-standard network setup specified")
        if network["interface"] == "eth":
            syslog.syslog("Configuring static Ethernet connection")
            ethernet_found = False
            path = ''
            services = manager.GetServices()
            for entry in services:
                path = entry[0]
                _ = entry[1]
                ethernet_found = path.startswith("/net/connman/service/ethernet")
                if ethernet_found:
                    break

            if ethernet_found:
                syslog.syslog("Ethernet device identified has entry point " + path)
                service = dbus.Interface(bus.get_object('net.connman', path), 'net.connman.Service')
                _ = service.GetProperties()
                syslog.syslog("Setting ip, mask and gateway")
                ipv4_configuration = {
                    "Method": make_variant("manual"),
                    "Address": make_variant(network["ip"]),
                    "Netmask": make_variant(network["mask"]),
                    "Gateway": make_variant(network["gw"])
                }
                service.SetProperty("IPv4.Configuration", ipv4_configuration)
                time.sleep(2)
                syslog.syslog("Setting nameservers")
                dns = {network["dns1"], network["dns2"]}
                # duplicate SetProperty message works around connman dns forwarder bug
                service.SetProperty("Nameservers.Configuration",
                                    dbus.Array(dns, signature=dbus.Signature('s')))
                service.SetProperty("Nameservers.Configuration",
                                    dbus.Array(dns, signature=dbus.Signature('s')))

            else:
                syslog.syslog("Cannot find a wired network adapter")
                sys.exit(4)

        if network["interface"] == "wlan":
            syslog.syslog("Configuring Wireless connection")
            syslog.syslog("Enabling wireless connection")
            technology = dbus.Interface(
                bus.get_object("net.connman", "/net/connman/technology/wifi"),
                "net.connman.Technology"
            )
            try:
                technology.SetProperty("Powered", True)
            except dbus.DBusException:
                syslog.syslog("Wireless connectivity is already enabled. "
                              "If you are running this script again, you probably "
                              "shouldn't be unless you know what you are doing")
            time.sleep(2)
            syslog.syslog("Scanning for WiFi networks")
            try:
                technology.Scan()
            except dbus.DBusException:
                # technology.Scan() will sometimes time out if connman is "busy".
                # Catch exception and retry.
                syslog.syslog("First technology.Scan() failed, retrying...")
                try:
                    time.sleep(2)
                    technology.Scan()
                except dbus.DBusException:
                    syslog.syslog("Second technology.Scan() failed.")
            time.sleep(10)

            syslog.syslog("Mapping SSID to service")
            network_found = False
            service = None
            services = manager.GetServices()
            for entry in services:
                path = entry[0]
                properties = entry[1]
                if network["hidden"] == "false":
                    if not path.startswith("/net/connman/service/wifi"):
                        continue
                    if "Name" not in properties:
                        syslog.syslog("Detected hidden wireless network during scan")
                        continue
                    svcname = properties["Name"]
                    syslog.syslog("Detected wireless network " +
                                  properties["Name"] + " during scan")
                    if network["ssid"] == svcname:
                        syslog.syslog("Found SSID entry point for " +
                                      network["ssid"] + " at " + path)
                        network_found = True
                        service = dbus.Interface(bus.get_object("net.connman", path),
                                                 "net.connman.Service")
                        break
                else:  # hidden network
                    syslog.syslog('Looking for hidden network entry ' + path)
                    if network["keytype"] == "0":
                        if path.endswith("hidden_managed_none"):
                            syslog.syslog("Found open but hidden network at " + path)
                            network_found = True
                            service = dbus.Interface(bus.get_object("net.connman", path),
                                                     "net.connman.Service")
                            break
                    else:
                        if path.endswith("_hidden_managed_psk"):
                            syslog.syslog("Found (psk) encrypted hidden network at " + path)
                            network_found = True
                            service = dbus.Interface(bus.get_object("net.connman", path),
                                                     "net.connman.Service")
                            break

            if network_found is False:
                syslog.syslog("Unable to find wireless network " + network["ssid"])
                sys.exit(5)

            agent_required = not network["keytype"] == "0" or network["hidden"] == "true"
            process = None
            if agent_required:
                syslog.syslog("Hidden and or protected network need to setup an agent")
                with open("/tmp/preseed_data", "w", encoding="utf-8") as preseed_data:
                    if not network["keytype"] == "0":
                        syslog.syslog('Protected Network - Writing key to preseed_data for agent')
                        preseed_data.write(network["keyvalue"])
                    preseed_data.write('\n')
                    if network["hidden"] == "true":
                        syslog.syslog('Encrypted Network - Writing SSID to preseed_data for agent')
                        preseed_data.write(network["ssid"])
                    preseed_data.write('\n')
                process = subprocess.Popen([sys.executable, '/usr/bin/preseed-agent', 'fromfile'])

            syslog.syslog("Attempting connection now")
            connected = 1
            while service and connected != 0 and connected < 16:
                try:
                    service.Connect(timeout=15000)
                    connected = 0
                except dbus.DBusException as e:
                    if len(e.args) > 0 and e.args[0] == 'Not registered' and agent_required:
                        connected += 1
                        time.sleep(1)
                        syslog.syslog('Connection agent not started yet, waiting a second')
                    else:  # another type of exception jump out of the loop
                        connected = 2
                        syslog.syslog('DBusException Raised: ' + str(e))

            syslog.syslog("Cleaning up deleting /tmp/preseed_data and killing Agent")
            if agent_required:
                if os.path.exists('/tmp/preseed_data'):
                    os.remove('/tmp/preseed_data')
                if process:
                    process.kill()

            if not connected == 0:
                if connected == 15:  # agent has not started in time
                    sys.exit(7)
                else:
                    sys.exit(6)  # could not connect

            if network["auto"] == "false":
                syslog.syslog("Configuring static wireless connection")
                _ = service.GetProperties()
                syslog.syslog("Setting ip, mask and gateway")
                ipv4_configuration = {
                    "Method": make_variant("manual"),
                    "Address": make_variant(network["ip"]),
                    "Netmask": make_variant(network["mask"]),
                    "Gateway": make_variant(network["gw"])
                }
                service.SetProperty("IPv4.Configuration", ipv4_configuration)
                time.sleep(2)
                syslog.syslog("Setting nameservers")
                dns = {network["dns1"], network["dns2"]}
                # duplicate SetProperty message works around connman dns forwarder bug
                service.SetProperty("Nameservers.Configuration",
                                    dbus.Array(dns, signature=dbus.Signature('s')))
                service.SetProperty("Nameservers.Configuration",
                                    dbus.Array(dns, signature=dbus.Signature('s')))


main()
