#!/usr/bin/env python
# -*- 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: userinfo -a <user> -u <user> -c <user> -r
#    -a    Set the authentication user (the user whose password you are
#          going to enter)
#    -h    Set the host to display
#    -l    list all hosts and their status
#    -f    list all SSH fingerprints

import time, os, pwd, sys, getopt, ldap, crypt, readline, copy;
from tempfile import mktemp
from os import O_CREAT, O_EXCL, O_WRONLY
from userdir_ldap import *;

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),
      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 AttrInfo.has_key(at):
         if AttrInfo[at][1] == 0:
            print "      %-18s:" % (AttrInfo[at][0]),
	    for x in Attrs[1][at]:
	       print "'%s'" % (x),
            print;
         else:
            OrderedIndex[AttrInfo[at][1]][1] = Attrs[1][at];

   Keys = OrderedIndex.keys();
   Keys.sort();
   for at in Keys:
      if at < 100 or RootMode != 0:
         print " %3u) %-18s: " % (at,OrderedIndex[at][0]),
         for x in OrderedIndex[at][1]:
            print "'%s'" % (re.sub('[\n\r]','?',x)),
         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] = ['']
   print "%-12s  %-10s  %-38s  %-25s %s" % (\
      Attrs[1]['host'][0], \
      Attrs[1]['architecture'][0], \
      Attrs[1]['distribution'][0], \
      Attrs[1]['access'][0], \
      Attrs[1]['status'][0])

# 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 = raw_input("New? ");

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

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

      print;
      Attrs[1][Attr] = [""];
      return;

   # Set a new value
   print "Setting.",;
   l.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 not Attrs[1].has_key(Attr):
      Attrs[1][Attr] = [];

   Attrs[1][Attr].sort();
   print "Old values: ",Attrs[1][Attr];

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

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

   # Delete
   if (Mode == "D"):
      print "Deleting.",;
      try:
         l.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.",;
   l.modify_s(HostDn,[(ldap.MOD_ADD,Attr,NewValue)]);
   Attrs[1][Attr].append(NewValue);
   print;

def CalcTempFile():
   unique = 0
   while unique == 0:
      name = mktemp()
      try:
         fd = os.open(name, O_CREAT | O_EXCL | O_WRONLY, 0600)
      except OSError:
         continue
      os.close(fd)
      unique = 1
   return name


# 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, 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 != ""):
   l = passwdAccessLDAP(BaseDn, BindUser)
else:
   l = connectLDAP()
   l.simple_bind_s("","")

if ListMode == 1:
   Attrs = l.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 = l.search_s(HostBaseDn,ldap.SCOPE_ONELEVEL,"host=" + Host)
   else:
      Attrs = l.search_s(HostBaseDn,ldap.SCOPE_ONELEVEL,"host=*")
   hosts = []
   for hAttrs in Attrs:
      hosts.append(hAttrs[1]['host'][0])
   hosts.sort()

   tmpfile = CalcTempFile()
   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']:
                  tmp = open(tmpfile, 'w')
                  tmp.write(key + '\n')
                  tmp.close()
                  fp = os.popen('/usr/bin/ssh-keygen -l -f ' + tmpfile, "r")
                  input = fp.readline()
                  fp.close()
                  fingerprint = input.split(' ')
                  print "%s %s root@%s" % (fingerprint[0], fingerprint[1], host)
   os.unlink(tmpfile)
   sys.exit(0)

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

# Query the server for all of the attributes
Attrs = l.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 = raw_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 = raw_input("Host? ");
      if NewHost == "":
         continue;
      NAttrs = l.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 Change who we are looking at
   if (Response == 'n' or Response == 'N'):
      NewHost = raw_input("Host? ");
      if NewHost == "":
         continue;
      NAttrs = l.search_s(HostBaseDn,ldap.SCOPE_ONELEVEL,"host=" + NewHost);
      if len(NAttrs) != 0:
         print "Host",NewHost,"already exists.";
         continue;
      NewHostName = raw_input("Hostname? ");
      if NewHost == "":
         continue;
      Dn = "host=" + NewHost + "," + HostBaseDn;
      l.add_s(Dn,[("host", NewHost),
                  ("hostname", NewHostName),
                  ("objectClass", ("top", "debianServer"))]);

      # Switch
      NAttrs = l.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 = raw_input("Attr? ");
      ChangeAttr(Attrs[0],Attr);
      continue;

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

   # Convert the integer response
   try:
      ID = int(Response);
      if (not OrderedIndex.has_key(ID) 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]);
