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

# Copyright (C) 2012-2014 Stéphane Graber
# Author: Stéphane Graber <stgraber@ubuntu.com>

# 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 can find the license on Debian systems in the file
# /usr/share/common-licenses/GPL-2

# NOTE: To remove once the API is stabilized
import warnings
warnings.filterwarnings("ignore", "The python-lxc API isn't yet stable")

import argparse
import configparser
import gettext
import glob
import os
import re
import shutil
import subprocess
import sys

_ = gettext.gettext
gettext.textdomain("edubuntu-server-deploy")

# Some constants
EDUBUNTU_CONF = "/etc/edubuntu-server"
EDUBUNTU_GLOBAL_CONF = "/etc/edubuntu-server/edubuntu-server.conf"
EDUBUNTU_CONTAINERS_CONF = "/etc/edubuntu-server/containers.conf"
ALL_SERVICES = ["manager", "directory", "dhcp", "rdns", "ltsp"]
LAN_SERVICES = ["dhcp"]
MANDATORY_SERVICES = ["manager", "directory", "rdns"]
UPSTART_JOB = "edubuntu-server"
MIN_CLIENTS = 30
LXC_TEMPLATE = "tpl-edubuntu-server"
LXC_LIB = "/var/lib/lxc/"
LXC_CACHE = "/var/cache/lxc/"


# Main functions
def isValidDomain(domain):
    if len(domain) > 255:
        return False

    allowed = re.compile("(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
    return all(allowed.match(sub) for sub in domain.split("."))


def config_to_environ(config):
    environ = {}
    for section in config.sections():
        for key in config.options(section):
            environ["%s_%s" % (section, key)] = config.get(section, key)
    return environ

# Begin parsing the command line
parser = argparse.ArgumentParser(
    description=_("Edubuntu server deployment script"),
    formatter_class=argparse.RawTextHelpFormatter)

# Optional arguments
parser.add_argument("--bridge-name", type=str, default="edubr0",
                    help=_("The name of your LAN interface."))
parser.add_argument("--bridge-interface", type=str,
                    help=_("The name of the internal Edubuntu bridge."))
parser.add_argument("--cleanup", action="store_true",
                    help=_("Remove any existing deployment and start over."))

## The .test TLD is reserved for local usage per RFC2606
parser.add_argument("--domain", type=str, default="edubuntu.test",
                    help=_("DNS domain name for your Edubuntu network."))

parser.add_argument("--service", default=[],
                    type=str, action="append",
                    help=_("The name of a service to deploy "
                           "(can be used multiple times)."))

## The 10.128.0.0/24 subnet is an RFC1918 subnet that's not currently used
## by any other Ubuntu packages in their default configuration
parser.add_argument("--subnet", type=str, default="10.0.128.0/24",
                    help=_("A valid CIDR subnet for your Edubuntu network."))

args = parser.parse_args()
if not args.service:
    args.service = ALL_SERVICES

# Basic requirements check
## The user needs to be uid 0
if not os.geteuid() == 0:
    print(_("You must be root to run this script. Try running: sudo %s" %
            (sys.argv[0])))
    sys.exit(1)

# Only import ipaddress and lxc now so we don't need to build-depend on them
import ipaddress
import lxc

## The Edubuntu server needs to be disabled
job_status = subprocess.Popen(["initctl", "status", UPSTART_JOB],
                              stdout=subprocess.PIPE, stderr=None,
                              universal_newlines=True)
job_status_retval = job_status.wait()

if job_status_retval != 0:
    print(_("Something is wrong with the '%s' upstart job." % UPSTART_JOB))
    sys.exit(1)

if job_status.stdout.read().strip() != "%s stop/waiting" % UPSTART_JOB:
    print(_("The '%s' upstart job is still running." % UPSTART_JOB))
    sys.exit(1)

## Look for the LXC template
if not os.path.exists("/var/lib/lxc/%s/" % LXC_TEMPLATE):
    print(_("""The LXC template '%s' doesn't exist.
Please run: sudo edubuntu-server-build-template""" % LXC_TEMPLATE))
    sys.exit(1)

# Argument validation
## Validating the services
if not set(args.service).issuperset(MANDATORY_SERVICES):
    print(_("The following services are mandatory: %s" %
            ", ".join(MANDATORY_SERVICES)))
    sys.exit(1)

## Validating the network interfaces
current_bridges = [bridge.split("/")[-2] for bridge in
                   glob.glob("/sys/class/net/*/bridge")]
current_interfaces = [interface.split("/")[-2] for interface in
                      glob.glob("/sys/class/net/eth*/type")]

if args.bridge_name in current_bridges:
    print(_("The '%s' bridge already exists." % args.bridge_name))
    sys.exit(1)

if args.bridge_interface and args.bridge_interface not in current_interfaces:
    print(_("The interface '%s' doesn't exist." % args.bridge_interface))
    sys.exit(1)

## Validating the subnet
try:
    subnet = ipaddress.IPv4Network(args.subnet)
except ipaddress.AddressValueError:
    print(_("Invalid IPv4 subnet '%s'." % args.subnet))
    sys.exit(1)

### 1 host + services + network and broadcast
subnet_minsize = 1 + len(args.service) + 2
if not set(args.service).isdisjoint(set(LAN_SERVICES)):
    subnet_minsize += MIN_CLIENTS

if subnet.numhosts < subnet_minsize:
    print(_("You must have a minimum of '%s' addresses available." %
            subnet_minsize))
    sys.exit(1)

## Validating the domain
if args.domain[-1:] == ".":
    args.domain = args.domain[:-1]

if not isValidDomain(args.domain):
    print(_("Invalid domain name '%s'" % args.domain))
    sys.exit(1)

## Check for existing containers
for entry in glob.glob("%s/edubuntu-server-*" % LXC_LIB):
    if not args.cleanup:
        print(_("An edubuntu-server container already exists: %s." %
                entry))
        print(_("You may want to run the deployment script with --cleanup"))
        sys.exit(1)

## Check for existing configuration
for entry in (EDUBUNTU_GLOBAL_CONF, EDUBUNTU_CONTAINERS_CONF):
    if os.path.exists(entry) and not args.cleanup:
        print(_("A configuration file already exists: %s." % entry))
        sys.exit(1)

#NOTE: Writing stuff to disk starts now, everything before is read-only

# Starting the actual setup
## Cleanup
for entry in glob.glob("%s/edubuntu-server-*" % LXC_LIB) + \
        [EDUBUNTU_GLOBAL_CONF, EDUBUNTU_CONTAINERS_CONF]:
    if os.path.exists(entry):
        if os.path.isdir(entry):
            shutil.rmtree(entry)
        else:
            os.remove(entry)

## Preparation
if not os.path.exists(EDUBUNTU_CONF):
    os.mkdir(EDUBUNTU_CONF)

## Update template for bridge interface
template = lxc.Container(LXC_TEMPLATE)
template.set_config_item("lxc.network.0.link", args.bridge_name)
template.save_config()

## Save the global configuration
global_config = configparser.ConfigParser()

### Network
global_config.add_section("network")
global_config.set('network', 'bridge_name', args.bridge_name)
if args.bridge_interface:
    global_config.set('network', 'bridge_interface', args.bridge_interface)
global_config.set('network', 'subnet', str(subnet))
global_config.set('network', 'gateway', str(subnet[1]))
global_config.set('network', 'clients_start', str(
                  subnet[1 + len(args.service)]))

### Domain
global_config.add_section("domain")
global_config.set('domain', 'fqdn', args.domain)

fd = open(EDUBUNTU_GLOBAL_CONF, "w+")
global_config.write(fd)
fd.close()

## Create the conainers and add them to the container configuration
containers_config = configparser.ConfigParser()

nextip = subnet[2]

# First pass to generate the IP for all services
service_ips = {}
for service in args.service:
    container_name = "edubuntu-server-%s01" % service
    containers_config.add_section(container_name)
    containers_config.set(container_name, "ipv4", str(nextip))

    if not service in service_ips:
        service_ips[service] = []
    service_ips[service].append(str(nextip))

    nextip += 1

global_config.add_section("services")
for entry, ips in service_ips.items():
    global_config.set("services", entry, ", ".join(ips))

# Rest of the configuration
for service in args.service:
    # Create the container
    container_name = "edubuntu-server-%s01" % service
    template = lxc.Container(LXC_TEMPLATE)
    container = template.clone(container_name)

    # Prepare service configuration
    config_script = "/usr/share/edubuntu-server/services/%s/configure" % (
        service)

    environ = {}
    if os.path.exists(config_script):
        local_environ = dict(os.environ).copy()
        local_environ.update(config_to_environ(global_config))
        configure = subprocess.Popen([config_script],
                                     env=local_environ,
                                     universal_newlines=True,
                                     stdout=subprocess.PIPE)

        if configure.wait() != 0:
            print(_("Failed to run configure: %s" % container_name))
            sys.exit(1)

        for line in configure.stdout.read().strip().split("\n"):
            key, value = line.split("=", 1)
            environ[key] = value

    # Run the setup script
    setup_script = "/usr/share/edubuntu-server/services/%s/setup" % service
    if os.path.exists(setup_script):
        shutil.copy(setup_script, "%s/%s/rootfs/setup" %
                    (LXC_LIB, container_name))
        local_environ = dict(os.environ).copy()
        local_environ.update(environ)
        setup = subprocess.Popen(["lxc-unshare",
                                  "-s", "PID|MOUNT|IPC|UTSNAME",
                                  "chroot", "%s/%s/rootfs/" %
                                  (LXC_LIB, container_name),
                                  "--", "/setup"],
                                 env=local_environ)

        if setup.wait() != 0:
            print(_("Failed to run setup: %s" % container_name))
            sys.exit(1)

        os.remove("%s/%s/rootfs/setup" % (LXC_LIB, container_name))

    # Configure the network
    with open("%s/%s/rootfs/etc/network/interfaces" %
              (LXC_LIB, container_name), "w+") as fd:
        fd.write("# This file describes the network interfaces "
                 """available on your system
# and how to activate them. For more information, see interfaces(5).

# The loopback network interface
auto lo
iface lo inet loopback

auto eth0
iface eth0 inet static
    address %s
    netmask %s
    gateway %s

    dns-domain %s
    dns-nameservers %s
""" % (containers_config.get(container_name, "ipv4"),
       subnet.netmask, subnet[1],
       global_config.get("domain", "fqdn"),
       global_config.get("services", "rdns").replace(",", "")))

    # Update config
    containers_config.set(container_name, "autostart", "true")
    for key, value in environ.items():
        containers_config.set(container_name, key, value)

fd = open(EDUBUNTU_CONTAINERS_CONF, "w+")
containers_config.write(fd)
fd.close()
