#!/usr/bin/python3

#
# Copyright (C) 2022 Nethesis S.r.l.
# http://www.nethesis.it - nethserver@nethesis.it
#
# This script is part of NethServer.
#
# NethServer 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 3 of the License,
# or any later version.
#
# NethServer 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 NethServer.  If not, see COPYING.
#

import time
import ssl
import sys
import subprocess
import argparse
import json
import hashlib
import uuid
import os
import socket
from urllib import request, parse, error

def call(api_endpoint, action, token, data, tlsverify, hidden=False):
    # Prepare SSL context
    ctx = ssl.create_default_context()
    if not tlsverify:
        ctx.check_hostname = False
        ctx.verify_mode = ssl.CERT_NONE
    jdata = json.dumps({"action": action, "data": data, "extra": {
                       "isNotificationHidden": hidden,
                       "title": action}}).encode('utf8')
    req = request.Request(f'{api_endpoint}/api/cluster/tasks', data=jdata)
    req.add_header('Content-Type', 'application/json')
    req.add_header('Authorization', f'Bearer {token}')
    post = request.urlopen(req, context=ctx)
    post_response = json.loads(post.read())
    # wait for the cluster queue to grab the request
    time.sleep(0.5)
    if post_response["code"] == 201:
        get_response = {"code": 201}
        task_id = post_response["data"]["id"]

        # wait until the response is ready
        watchdog = 0
        while get_response["code"] == 201:
            if watchdog >= 30:
                print("No server response", file=sys.stderr)
                sys.exit(1)
            try:
                req = request.Request(f'{api_endpoint}/api/cluster/task/{task_id}/status')
                req.add_header('Content-Type', 'application/json')
                req.add_header('Authorization', f'Bearer {token}')
                get = request.urlopen(req, context=ctx)
                get_response = json.loads(get.read())
            except:
                time.sleep(5)
                pass
            finally:
                watchdog = watchdog + 1

        return get_response
    return None


parser = argparse.ArgumentParser()
parser.add_argument('host')
parser.add_argument('username', default="admin")
parser.add_argument('password', default="Nethesis,1234")
# user domain used to rename the directory.nh to another baseDN
parser.add_argument('user_domain', default="")
parser.add_argument('--no-tlsverify', dest='tlsverify', action='store_false', default=True)

args = parser.parse_args()

# Prepare login credentials
loginobj = {
    "username": args.username,
    "password": args.password,
}
data=json.dumps(loginobj).encode('utf8')

# Prepare SSL context
ctx = ssl.create_default_context()
if not args.tlsverify:
    ctx.check_hostname = False
    ctx.verify_mode = ssl.CERT_NONE

api_endpoint = f'https://{args.host}/cluster-admin'

# POST login request
req = request.Request(f'{api_endpoint}/api/login', data=data)
req.add_header('Content-Type', 'application/json')

try:
    resp = request.urlopen(req, context=ctx)
except error.URLError as ex:
    print("ns8-join:", str(ex), file=sys.stderr)
    sys.exit(1)

payload = json.loads(resp.read())

# Default values for account provider configuration:
account_provider_domain = ""
account_provider_external = ""

# Retrieve account provider information
account_provider_json = subprocess.check_output(['/usr/sbin/account-provider-test', 'dump'], encoding='utf-8')
account_provider_config = json.loads(account_provider_json)

# Retrieve NS8 cluster information for later validation
cluster = call(api_endpoint, "get-cluster-status", payload['token'], {}, args.tlsverify, hidden=True)
domains  = call(api_endpoint, "list-user-domains", payload['token'], {"extra_fields":["base_entryuuid"]}, args.tlsverify, hidden=True)

# Test if the cluster has a room for an active directory (needs at least one node free)
# only if the account provider is AD and local
if account_provider_config['isAD'] == '1':
    nsdc_ip_address = ""
    sssd_props_json = subprocess.check_output(['/sbin/e-smith/config', 'printjson', 'sssd'], encoding='utf-8')
    account_provider_domain = json.loads(sssd_props_json)['props']['Realm'].lower()
    try:
        # If the account provider is AD, assume it is local and try to get nsdc IP address, otherwise skip.
        nsdc_props_json = subprocess.check_output(['/sbin/e-smith/config', 'printjson', 'nsdc'], encoding='utf-8')
        nsdc_ip_address = json.loads(nsdc_props_json)['props']['IpAddress']
        # Initialize the flag
        all_installed = True
        # Parse all nodes
        for node in cluster["data"]["output"]["nodes"]:
            node_id = node["id"]
            node["adProvider_installed"] = False  # Default to False
            # Check each domain for AD providers
            for domain in domains['data']['output']['domains']:
                if domain["schema"] == "ad":
                    for provider in domain["providers"]:
                        if provider["node"] == node_id:
                            node["adProvider_installed"] = True  # Mark as installed
            # no AD installed set the flag
            if not node["adProvider_installed"]:
                all_installed = False
        # If all nodes have providers
        if all_installed:
            print("no_available_node_for_samba_provider", file=sys.stderr)
            sys.exit(1)
    except (TypeError, json.JSONDecodeError):
        # Ignore missing nsdc/IpAddress prop, assuming the provider is
        # external.
        account_provider_external = "1"
        # Ensure the external AD domain is already configured in NS8:
        for domain in domains['data']['output']['domains']:
            if domain.get("schema", "") != "ad":
                continue # ignore non-AD domains
            elif domain["name"] == account_provider_domain:
                break # Domain match found.
        else:
            # No domain match found.
            print("ns8-join: a matching external AD domain was not found in NS8.", file=sys.stderr)
            sys.exit(1)
elif account_provider_config['isLdap'] == '1':
    if '127.0.0.1' in account_provider_config['LdapURI']:
        # We have a local OpenLDAP account provider. Acquire the domain
        # name for migration from UI.
        if not args.user_domain:
            print("ns8-join: user_domain is required for OpenLDAP account provider", file=sys.stderr)
            sys.exit(1)
        account_provider_domain = args.user_domain.lower()
    else:
        account_provider_external = "1"
        # Check if remote RFC2307 account provider is correctly configured
        # in NS8. Acquire the domain name for migration from NS8 existing
        # domain.
        matching_domains = []
        for domain in domains['data']['output']['domains']:
            if domain.get("schema", "") != "rfc2307":
                continue # ignore non-rfc2307 domains
            elif domain["location"] != "external":
                continue # ignore internal domains
            elif domain["base_dn"].lower() == account_provider_config["BaseDN"].lower():
                account_provider_domain = domain["name"] # Domain match found.
                matching_domains.append(domain["name"])
        if len(matching_domains) == 0:
            # No domain match found.
            print("ns8-join: a matching external LDAP domain was not found in NS8.", file=sys.stderr)
            sys.exit(1)
        elif len(matching_domains) > 1:
            # Too much matches: anomaly.
            print("ns8-join: multiple LDAP domain matches were found: ", ", ".join(matching_domains), file=sys.stderr)
            sys.exit(1)

# Generate a new Wireguard key. If add-node succeedes, but VPN connection
# fails, we must not reuse the same key to avoid conflicts.
tmpmask = os.umask(0o077)
with open('/var/lib/nethserver/secrets/ns8wg', 'w') as wgfp:
    subprocess.run(['wg', 'genkey'], stdout=wgfp, check=True)
with open('/var/lib/nethserver/secrets/ns8wg', 'r') as wgfp:
    pub_key = subprocess.check_output(['wg', 'pubkey'], stdin=wgfp, encoding='UTF-8').rstrip("\n")
os.umask(tmpmask) ; del tmpmask
node_pw = str(uuid.uuid4())
node_pwh = hashlib.sha256(node_pw.encode('ASCII')).hexdigest()

# Prepare arguments for add-node
data = {
    "core_version": "0.0.0-ns7", # Semver value, required by Core 3.6+
    "node_pwh": node_pwh,
    "public_key": pub_key,
    "endpoint": "",
    "flags": ["nomodules"]
}
# Execute add-node
ret = call(api_endpoint, "add-node", payload['token'], data, args.tlsverify)

if not ret['code'] == 200:
    print("Request has failed: {}".format(ret), file=sys.stderr)
    sys.exit(1)

if not ret['data']['exit_code'] == 0:
    print("Task has failed: {}".format(ret['data']), file=sys.stderr)
    sys.exit(1)

ret = ret['data']['output']

# Check the leader endpoint can be resolved with DNS:
leader_epaddress, leader_epport = ret["leader_endpoint"].rsplit(':')
try:
    taddr = socket.getaddrinfo(leader_epaddress, leader_epport, socket.AF_INET, socket.SOCK_DGRAM)
except socket.gaierror:
    call(api_endpoint, "remove-node", payload['token'], {"node_id": ret['node_id']}, args.tlsverify)
    print("ns8-join: The cluster VPN endpoint name cannot be resolved. Please check DNS record and resolution of: %s" % (leader_epaddress), file=sys.stderr)
    sys.exit(2)

# create the ns7admin user password
ns7admin_pw = str(uuid.uuid4())
ns7admin_pwh = hashlib.sha256(ns7admin_pw.encode('ASCII')).hexdigest()
ns7admin_user = "ns7admin"+str(ret['node_id'])

# Create the ns7admin user
data_ns7admin = {
        "grant": [
            {
                "on": "*",
                "role": "owner"
            }
        ],
        "password_hash": ns7admin_pwh,
        "set": {
            "display_name": ns7admin_user,
        },
        "user": ns7admin_user
    }

add_user_response = call(api_endpoint, "add-user", payload['token'], data_ns7admin, args.tlsverify)
if add_user_response['data']['exit_code'] != 0:
    # we need to leave the cluster if the user ns7admin cannot be added
    print(f"Task add_user {ns7admin_user} has failed:", add_user_response, file=sys.stderr)
    subprocess.run(['/usr/sbin/ns8-leave'])
    sys.exit(1)

# Save config inside config db
subprocess.run(["/sbin/e-smith/config", "setprop", "wg-quick@ns8", "Address", ret["ip_address"], "RemoteEndpoint", ret["leader_endpoint"], "RemoteKey", ret["leader_public_key"], "RemoteNetwork", ret['network'], "status", "enabled"], check=True)
subprocess.run(["/sbin/e-smith/config", "setprop", "ns8", "Host", leader_epaddress, "User", ns7admin_user, "Password", ns7admin_pw, "TLSVerify", "enabled" if args.tlsverify else "disabled", "LeaderIpAddress", ret['leader_ip_address']], check=True)

# Save agent environment
with open('/var/lib/nethserver/nethserver-ns8-migration/agent.env', 'w') as fp:
    fp.write(f"REDIS_ADDRESS={ret['leader_ip_address']}:6379\n")
    fp.write(f"AGENT_ID=node/{ret['node_id']}\n")
    fp.write(f"REDIS_USER=node/{ret['node_id']}\n")
    fp.write(f"REDIS_PASSWORD={node_pw}\n")

with open('/var/lib/nethserver/nethserver-ns8-migration/environment', 'w') as fp:
    fp.write(f"NODE_ID={ret['node_id']}\n")

# Start the VPN on device ns8
subprocess.run(['/sbin/e-smith/signal-event', 'nethserver-ns8-migration-save'], check=True)

# Endpoint switch: pass through the VPN
api_endpoint = f"http://{ret['leader_ip_address']}:9311"

#
# Configure NS7 local account provider as external user domain provider in NS8
#
if account_provider_external != "":
    pass # External account providers must be already configured: nothing to do.
elif account_provider_config['isAD'] == '1':
        # Push a route to reach NSDC IP address through the cluster VPN
        update_routes_request = {"add": [{"ip_address": nsdc_ip_address, "node_id": ret['node_id']}]}
        update_routes_response = call(api_endpoint, "update-routes", payload['token'], update_routes_request, args.tlsverify)
        if update_routes_response['data']['exit_code'] != 0:
            print("Task update_routes has failed:", update_routes_response, file=sys.stderr)
            sys.exit(1)
        # Update nsdc route for wireguard VPN
        subprocess.run(['/sbin/e-smith/expand-template', '/var/lib/machines/nsdc/etc/systemd/network/green.network'], check=True)
        subprocess.run(['systemctl', '-M', 'nsdc', 'restart', 'systemd-networkd'], check=True)
        # Configure NSDC as account provider of an external user domain:
        add_external_domain_request = {
            "domain": account_provider_domain,
            "protocol": "ldap",
            "host": nsdc_ip_address,
            "port": account_provider_config['port'],
            "schema": 'ad',
            "bind_dn": account_provider_config['BindDN'],
            "bind_password": account_provider_config['BindPassword'],
            "base_dn": account_provider_config['BaseDN'],
            "tls": True,
            "tls_verify": False,
        }
        add_external_domain_response = call(api_endpoint, "add-external-domain", payload['token'], add_external_domain_request, args.tlsverify)
        if add_external_domain_response['data']['exit_code'] != 0:
            # we need to leave the cluster if the external domain cannot be added
            error = add_external_domain_response['data']['output'][0].get('error', '')
            value = add_external_domain_response['data']['output'][0].get('value', '')
            message = f"Task add_external_domain has failed: reason: {error} value: {value}"
            print(message, file=sys.stderr)
            subprocess.run(['/usr/sbin/ns8-leave']) # leave the cluster, we failed to connect to the external domain
            sys.exit(1)
elif account_provider_config['isLdap'] == '1':
    add_external_domain_request = {
        "domain": account_provider_domain,
        "protocol": "ldap",
        "host": ret["ip_address"],
        "port": 636,
        "schema": 'rfc2307',
        "bind_dn": account_provider_config['BindDN'],
        "bind_password": account_provider_config['BindPassword'],
        "base_dn": account_provider_config['BaseDN'],
        "tls": True,
        "tls_verify": False,
    }
    add_external_domain_response = call(api_endpoint, "add-external-domain", payload['token'], add_external_domain_request, args.tlsverify)
    if add_external_domain_response['data']['exit_code'] != 0:
        # we need to leave the cluster if the external domain cannot be added
        error = add_external_domain_response['data']['output'][0].get('error', '')
        value = add_external_domain_response['data']['output'][0].get('value', '')
        message = f"Task add_external_domain has failed: reason: {error} value: {value}"
        print(message, file=sys.stderr)
        subprocess.run(['/usr/sbin/ns8-leave']) # leave the cluster, we failed to connect to the external domain
        sys.exit(1)

with open('/var/lib/nethserver/nethserver-ns8-migration/environment', 'a') as fp:
    fp.write(f"USER_DOMAIN={account_provider_domain}\n")
    fp.write(f"ACCOUNT_PROVIDER_EXTERNAL={account_provider_external}\n")
