#!/usr/bin/python3
# -*- mode: python -*-

#   Copyright (c) 2000-2001  Jason Gunthorpe <jgg@debian.org>
#   Copyright (c) 2001       Ryan Murray <rmurray@debian.org>
#   Copyright (c) 2003       James Troup <troup@debian.org>
#   Copyright (c) 2004-2005  Joey Schulze <joey@infodrom.org>
#   Copyright (c) 2008,2009  Peter Palfrader <peter@palfrader.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., 675 Mass Ave, Cambridge, MA 02139, USA.

# This script is an interactive way to manipulate fields in the LDAP directory.
# When run it connects to the directory using the current users ID and fetches
# all the attributes for the first machine. It then formats them nicely and
# allows the user to change them.
#
#  Usage: ud-host -h <host> [-a <user>] [-l | -f]
#    -a    Set the authentication user (the user whose password you are
#          going to enter). Defaults to current user if not supplied.
#    -h    Set the host to display
#    -l    list all hosts and their status
#    -f    list all SSH fingerprints

from __future__ import print_function

import copy
import getopt
import os
import pwd
import re
import subprocess
import sys
import time

import ldap
from six.moves import input

from userdir_ldap.ldap import (
    BaseDn, HostBaseDn, connectLDAP, passwdAccessLDAP, GetAttr)


RootMode = 0
AttrInfo = {"description": ["Machine Descr.", 1],
            "hostname": ["Host names", 2],
            "status": ["Status", 3],
            "l": ["Location", 4],
            "sponsor": ["Sponsors", 5],
            "distribution": ["Distribution", 6],
            "access": ["Access", 7],
            "admin": ["Admin", 8],
            "architecture": ["Architecture", 9],
            "machine": ["Machine Hardware", 10],
            "memory": ["Memory", 11],
            "disk": ["Disk", 12],
            "physicalHost": ["Physical Host", 13],
            "sshRSAHostKey": ["SSH Host Keys", 14],
            "bandwidth": ["Bandwidth", 15],
            "purpose": ["Purposes", 16],
            "allowedGroups": ["Groups", 17],
            "exportOptions": ["Export-Opts", 18],
            "ipHostNumber": ["IP Address", 19],
            "mXRecord": ["MXRecord", 20],
            "dnsTTL": ["dnsTTL", 21],
            "sshdistAuthKeysHost": ["extra authkeys ip", 22],
            }

AttrPrompt = {"description": ["Purpose of the machine"],
              "hostname": ["The hostnames for the box (ipv4/ipv6)"],
              "status": ["Blank if Up, explaination if not"],
              "l": ["Physical location"],
              "sponsor": ["Sponsors and their URLs"],
              "distribution": ["The distribution version"],
              "access": ["all, developer only, restricted"],
              "admin": ["Admin email address"],
              "architecture": ["Debian Architecture string"],
              "machine": ["Hardware description"],
              "memory": ["Installed RAM"],
              "disk": ["Disk Space, RAID levels, etc"],
              "physicalHost": ["The box hosting this virtual server"],
              "sshRSAHostKey": ["A copy of /etc/ssh/ssh_*host_key.pub"],
              "bandwidth": ["Available outbound"],
              "purpose": ["The purposes of this host"],
              "allowedGroups": ["allowed Groups on this host"],
              "exportOptions": ["additional export options"],
              "ipHostNumber": ["IP Addresses(es) of the machine"],
              "mXRecord": ["Mail Exchanger for this machine"],
              "dnsTTL": ["dns TTL value"],
              "sshdistAuthKeysHost": ["additional hosts for sshdist's authkeys file"],
              }

# Create a map of IDs to desc,value,attr
OrderedIndex = {}
for at in AttrInfo.keys():
    if AttrInfo[at][1] != 0:
        OrderedIndex[AttrInfo[at][1]] = [AttrInfo[at][0], "", at]
OrigOrderedIndex = copy.deepcopy(OrderedIndex)


# Print out the automatic time stamp information
def PrintModTime(Attrs):
    Stamp = GetAttr(Attrs, "modifyTimestamp", "")
    if len(Stamp) >= 13:
        Time = (int(Stamp[0:4]), int(Stamp[4:6]), int(Stamp[6:8]),
                int(Stamp[8:10]), int(Stamp[10:12]), int(Stamp[12:14]), 0, 0, -1)
        print("%-24s:" % ("Record last modified on"), time.strftime("%a %d/%m/%Y %X UTC", Time), end=" ")
        print("by", ldap.explode_dn(GetAttr(Attrs, "modifiersName"), 1)[0])

    Stamp = GetAttr(Attrs, "createTimestamp", "")
    if len(Stamp) >= 13:
        Time = (int(Stamp[0:4]), int(Stamp[4:6]), int(Stamp[6:8]),
                int(Stamp[8:10]), int(Stamp[10:12]), int(Stamp[12:14]), 0, 0, -1)
        print("%-24s:" % ("Record created on"), time.strftime("%a %d/%m/%Y %X UTC", Time))


# Display all of the attributes in a numbered list
def ShowAttrs(Attrs):
    print()
    PrintModTime(Attrs)

    for at in Attrs[1].keys():
        if at in AttrInfo:
            if AttrInfo[at][1] == 0:
                print("      %-18s:" % (AttrInfo[at][0]), end=" ")
                for x in Attrs[1][at]:
                    print("'%s'" % x.decode('utf-8'), end=" ")
                print()
            else:
                OrderedIndex[AttrInfo[at][1]][1] = Attrs[1][at]

    Keys = sorted(OrderedIndex.keys())
    for at in Keys:
        if at < 100 or RootMode != 0:
            print(" %3u) %-18s: " % (at, OrderedIndex[at][0]), end=" ")
            for x in OrderedIndex[at][1]:
                print("'%s'" % (re.sub('[\n\r]', '?', x.decode('utf-8'))), end=" ")
            print()


def Overview(Attrs):
    """Display a one-line overview for a given host"""
    for i in ['host', 'architecture', 'distribution', 'access', 'status']:
        if i not in Attrs[1].keys():
            Attrs[1][i] = [b'']
    print("%-12s  %-10s  %-38s  %-25s %s" % (
        Attrs[1]['host'][0].decode('utf-8'),
        Attrs[1]['architecture'][0].decode('utf-8'),
        Attrs[1]['distribution'][0].decode('utf-8'),
        Attrs[1]['access'][0].decode('utf-8'),
        Attrs[1]['status'][0].decode('utf-8')))


# Change a single attribute
def ChangeAttr(Attrs, Attr):
    if Attr in ["sponsor", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions", "ipHostNumber", "mXRecord", "sshdistAuthKeysHost"]:
        return MultiChangeAttr(Attrs, Attr)

    print("Old value: '%s'" % GetAttr(Attrs, Attr, ""))
    print("Press enter to leave unchanged and a single space to set to empty")
    NewValue = input("New? ")

    # Empty string
    if NewValue == "":
        print("Leaving unchanged.")
        return

    # Single space designates delete, trap the delete error
    if NewValue == " ":
        print("Deleting.", end="")
        try:
            lc.modify_s(HostDn, [(ldap.MOD_DELETE, Attr, None)])
        except ldap.NO_SUCH_ATTRIBUTE:
            pass

        print()
        Attrs[1][Attr] = [b""]
        return

    # Set a new value
    NewValue = NewValue.encode('utf-8')
    print("Setting.", end="")
    lc.modify_s(HostDn, [(ldap.MOD_REPLACE, Attr, NewValue)])
    Attrs[1][Attr] = [NewValue]
    print()


def MultiChangeAttr(Attrs, Attr):
    # Make sure that we have an entry
    if Attr not in Attrs[1]:
        Attrs[1][Attr] = []

    Attrs[1][Attr].sort()
    print("Old values: ", [x.decode('utf-8') for x in Attrs[1][Attr]])

    Mode = input("[D]elete or [A]dd? ").upper()
    if Mode != 'D' and Mode != 'A':
        return

    NewValue = input("Value? ")
    # Empty string
    if NewValue == "":
        print("Leaving unchanged.")
        return

    NewValue = NewValue.encode('utf-8')

    # Delete
    if Mode == "D":
        print("Deleting.", end=" ")
        try:
            lc.modify_s(HostDn, [(ldap.MOD_DELETE, Attr, NewValue)])
        except ldap.NO_SUCH_ATTRIBUTE:
            print("Failed")

        print()
        Attrs[1][Attr].remove(NewValue)
        return

    # Set a new value
    print("Setting.", end="")
    lc.modify_s(HostDn, [(ldap.MOD_ADD, Attr, NewValue)])
    Attrs[1][Attr].append(NewValue)
    print()


# Main program starts here
User = pwd.getpwuid(os.getuid())[0]
BindUser = User
ListMode = 0
FingerPrints = 0
Host = None
# Process options
try:
    (options, arguments) = getopt.getopt(sys.argv[1:], "nh:a:rlf")
except getopt.GetoptError as data:
    print(data)
    sys.exit(1)

for (switch, val) in options:
    if switch == '-h':
        Host = val
    elif switch == '-a':
        BindUser = val
    elif switch == '-r':
        RootMode = 1
    elif switch == '-n':
        BindUser = ""
    elif switch == '-l':
        BindUser = ""
        ListMode = 1
    elif switch == '-f':
        BindUser = ""
        FingerPrints = 1

if BindUser != "":
    lc = passwdAccessLDAP(BaseDn, BindUser)
else:
    lc = connectLDAP()
    lc.simple_bind_s("", "")

if ListMode == 1:
    Attrs = lc.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "host=*")
    hosts = []
    for hAttrs in Attrs:
        hosts.append(hAttrs[1]['host'][0])
    hosts.sort()

    print("%-12s  %-10s  %-38s  %-25s %s" % ("Host name", "Arch", "Distribution", "Access", "Status"))
    print("-" * 115)
    for host in hosts:
        for hAttrs in Attrs:
            if host == hAttrs[1]['host'][0]:
                Overview(hAttrs)
    sys.exit(0)
elif FingerPrints == 1:
    if Host is not None:
        Attrs = lc.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "host=" + Host)
    else:
        Attrs = lc.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "host=*")
    hosts = []
    for hAttrs in Attrs:
        hosts.append(hAttrs[1]['host'][0])
    hosts.sort()

    for host in hosts:
        for hAttrs in Attrs:
            if host == hAttrs[1]['host'][0]:
                if 'sshRSAHostKey' in hAttrs[1].keys():
                    for key in hAttrs[1]['sshRSAHostKey']:
                        keygen = subprocess.Popen(['/usr/bin/ssh-keygen', '-l', '-f', '-'],
                                                  stdin=subprocess.PIPE, stdout=subprocess.PIPE)
                        keygen.stdin.write(key + b'\n')
                        keygen.stdin.close()
                        keygen_output = keygen.stdout.readline()
                        keygen.wait()
                        fingerprint = keygen_output.decode('ascii').split(' ')
                        print("%s %s root@%s" % (fingerprint[0], fingerprint[1], host.decode('ascii')))
    sys.exit(0)

HostDn = "host=" + Host + "," + HostBaseDn

# Query the server for all of the attributes
Attrs = lc.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "host=" + Host)
if len(Attrs) == 0:
    print("Host", Host, "was not found.")
    Response = input("Create it? [Y]es/[N]o").upper()
    if Response != "Y":
        sys.exit(0)
    HostName = input("Hostname? ")
    if HostName == "":
        sys.exit(0)
    lc.add_s(HostDn, [("host", Host.encode('ascii')),
                      ("hostname", HostName.encode('ascii')),
                      ("objectClass", (b"top", b"debianServer"))])
    Attrs = lc.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "host=" + Host)
    if len(Attrs) == 0:
        print("Host", Host, "was not found.")
        sys.exit(0)

# repeatedly show the account configuration
while(1):
    ShowAttrs(Attrs[0])
    if BindUser == "":
        sys.exit(0)

    if RootMode == 1:
        print("   a) Arbitary Change")
    print("   n) New Host")
    print("   d) Delete Host")
    print("   u) Switch Hosts")
    print("   x) Exit")

    # Prompt
    Response = input("Change? ")
    if Response == "x" or Response == "X" or Response == "q" or \
       Response == "quit" or Response == "exit":
        break

    # Change who we are looking at
    if Response == 'u' or Response == 'U':
        NewHost = input("Host? ")
        if NewHost == "":
            continue
        NAttrs = lc.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "host=" + NewHost)
        if len(NAttrs) == 0:
            print("Host", NewHost, "was not found.")
            continue
        Attrs = NAttrs
        Host = NewHost
        HostDn = "host=" + Host + "," + HostBaseDn
        OrderedIndex = copy.deepcopy(OrigOrderedIndex)
        continue

    # Create a new entry and change to it
    if Response == 'n' or Response == 'N':
        NewHost = input("Host? ")
        if NewHost == "":
            continue
        NAttrs = lc.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "host=" + NewHost)
        if len(NAttrs) != 0:
            print("Host", NewHost, "already exists.")
            continue
        NewHostName = input("Hostname? ")
        if NewHostName == "":
            continue
        Dn = "host=" + NewHost + "," + HostBaseDn
        lc.add_s(Dn, [("host", NewHost.encode('ascii')),
                      ("hostname", NewHostName.encode('ascii')),
                      ("objectClass", (b"top", b"debianServer"))])

        # Switch
        NAttrs = lc.search_s(HostBaseDn, ldap.SCOPE_ONELEVEL, "host=" + NewHost)
        if len(NAttrs) == 0:
            print("Host", NewHost, "was not found.")
            continue
        Attrs = NAttrs
        Host = NewHost
        HostDn = "host=" + Host + "," + HostBaseDn
        OrderedIndex = copy.deepcopy(OrigOrderedIndex)
        continue

    # Handle changing an arbitary value
    if Response == "a":
        Attr = input("Attr? ")
        ChangeAttr(Attrs[0], Attr)
        continue

    if Response == 'd':
        Really = input("Really (type yes)? ")
        if Really != 'yes':
            continue
        print("Deleting", HostDn)
        lc.delete_s(HostDn)
        continue

    # Convert the integer response
    try:
        ID = int(Response)
        if ID not in OrderedIndex or (ID > 100 and RootMode == 0):
            raise ValueError
    except ValueError:
        print("Invalid")
        continue

    # Print the what to do prompt
    print("Changing LDAP entry '%s' (%s)" % (OrderedIndex[ID][0], OrderedIndex[ID][2]))
    print(AttrPrompt[OrderedIndex[ID][2]][0])
    ChangeAttr(Attrs[0], OrderedIndex[ID][2])
