ud-generate: deal with users without loginShell
[mirror/userdir-ldap.git] / ud-host
1 #!/usr/bin/env python
2 # -*- mode: python -*-
3
4 #   Copyright (c) 2000-2001  Jason Gunthorpe <jgg@debian.org>
5 #   Copyright (c) 2001       Ryan Murray <rmurray@debian.org>
6 #   Copyright (c) 2003       James Troup <troup@debian.org>
7 #   Copyright (c) 2004-2005  Joey Schulze <joey@infodrom.org>
8 #   Copyright (c) 2008,2009  Peter Palfrader <peter@palfrader.org>
9 #
10 #   This program is free software; you can redistribute it and/or modify
11 #   it under the terms of the GNU General Public License as published by
12 #   the Free Software Foundation; either version 2 of the License, or
13 #   (at your option) any later version.
14 #
15 #   This program is distributed in the hope that it will be useful,
16 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
17 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 #   GNU General Public License for more details.
19 #
20 #   You should have received a copy of the GNU General Public License
21 #   along with this program; if not, write to the Free Software
22 #   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
23
24 # This script is an interactive way to manipulate fields in the LDAP directory.
25 # When run it connects to the directory using the current users ID and fetches
26 # all the attributes for the first machine. It then formats them nicely and
27 # allows the user to change them.
28 #
29 #  Usage: userinfo -a <user> -u <user> -c <user> -r
30 #    -a    Set the authentication user (the user whose password you are
31 #          going to enter)
32 #    -h    Set the host to display
33 #    -l    list all hosts and their status
34 #    -f    list all SSH fingerprints
35
36 import time, os, pwd, sys, getopt, ldap, crypt, readline, copy, subprocess
37 from os import O_CREAT, O_EXCL, O_WRONLY
38 from userdir_ldap import *
39
40 RootMode = 0
41 AttrInfo = {"description": ["Machine Descr.", 1],
42             "hostname": ["Host names", 2],
43             "status": ["Status", 3],
44             "l": ["Location", 4],
45             "sponsor": ["Sponsors", 5],
46             "distribution": ["Distribution", 6],
47             "access": ["Access", 7],
48             "admin": ["Admin", 8],
49             "architecture": ["Architecture", 9],
50             "machine": ["Machine Hardware", 10],
51             "memory": ["Memory", 11],
52             "disk": ["Disk", 12],
53             "physicalHost": ["Physical Host", 13],
54             "sshRSAHostKey": ["SSH Host Keys", 14],
55             "bandwidth": ["Bandwidth", 15],
56             "purpose": ["Purposes", 16],
57             "allowedGroups": ["Groups", 17],
58             "exportOptions": ["Export-Opts", 18],
59             "ipHostNumber": ["IP Address", 19],
60             "mXRecord": ["MXRecord", 20],
61             "dnsTTL": ["dnsTTL", 21],
62             "sshdistAuthKeysHost": ["extra authkeys ip", 22],
63             }
64
65 AttrPrompt = {"description": ["Purpose of the machine"],
66               "hostname": ["The hostnames for the box (ipv4/ipv6)"],
67               "status": ["Blank if Up, explaination if not"],
68               "l": ["Physical location"],
69               "sponsor": ["Sponsors and their URLs"],
70               "distribution": ["The distribution version"],
71               "access": ["all, developer only, restricted"],
72               "admin": ["Admin email address"],
73               "architecture": ["Debian Architecture string"],
74               "machine": ["Hardware description"],
75               "memory": ["Installed RAM"],
76               "disk": ["Disk Space, RAID levels, etc"],
77               "physicalHost": ["The box hosting this virtual server"],
78               "sshRSAHostKey": ["A copy of /etc/ssh/ssh_*host_key.pub"],
79               "bandwidth": ["Available outbound"],
80               "purpose": ["The purposes of this host"],
81               "allowedGroups": ["allowed Groups on this host"],
82               "exportOptions": ["additional export options"],
83               "ipHostNumber": ["IP Addresses(es) of the machine"],
84               "mXRecord": ["Mail Exchanger for this machine"],
85               "dnsTTL": ["dns TTL value"],
86               "sshdistAuthKeysHost": ["additional hosts for sshdist's authkeys file"],
87               }
88
89 # Create a map of IDs to desc,value,attr
90 OrderedIndex = {}
91 for at in AttrInfo.keys():
92    if (AttrInfo[at][1] != 0):
93       OrderedIndex[AttrInfo[at][1]] = [AttrInfo[at][0], "", at]
94 OrigOrderedIndex = copy.deepcopy(OrderedIndex)
95
96 # Print out the automatic time stamp information
97 def PrintModTime(Attrs):
98    Stamp = GetAttr(Attrs,"modifyTimestamp","")
99    if len(Stamp) >= 13:
100       Time = (int(Stamp[0:4]),int(Stamp[4:6]),int(Stamp[6:8]),
101               int(Stamp[8:10]),int(Stamp[10:12]),int(Stamp[12:14]),0,0,-1)
102       print "%-24s:" % ("Record last modified on"), time.strftime("%a %d/%m/%Y %X UTC",Time),
103       print "by",ldap.explode_dn(GetAttr(Attrs,"modifiersName"),1)[0]
104
105    Stamp = GetAttr(Attrs,"createTimestamp","")
106    if len(Stamp) >= 13:
107       Time = (int(Stamp[0:4]),int(Stamp[4:6]),int(Stamp[6:8]),
108               int(Stamp[8:10]),int(Stamp[10:12]),int(Stamp[12:14]),0,0,-1)
109       print "%-24s:" % ("Record created on"), time.strftime("%a %d/%m/%Y %X UTC",Time)
110
111 # Display all of the attributes in a numbered list
112 def ShowAttrs(Attrs):
113    print
114    PrintModTime(Attrs)
115
116    for at in Attrs[1].keys():
117       if AttrInfo.has_key(at):
118          if AttrInfo[at][1] == 0:
119             print "      %-18s:" % (AttrInfo[at][0]),
120             for x in Attrs[1][at]:
121                print "'%s'" % (x),
122             print
123          else:
124             OrderedIndex[AttrInfo[at][1]][1] = Attrs[1][at]
125
126    Keys = OrderedIndex.keys()
127    Keys.sort()
128    for at in Keys:
129       if at < 100 or RootMode != 0:
130          print " %3u) %-18s: " % (at,OrderedIndex[at][0]),
131          for x in OrderedIndex[at][1]:
132             print "'%s'" % (re.sub('[\n\r]','?',x)),
133          print
134
135 def Overview(Attrs):
136    """Display a one-line overview for a given host"""
137    for i in ['host','architecture','distribution','access','status']:
138       if i not in Attrs[1].keys():
139          Attrs[1][i] = ['']
140    print "%-12s  %-10s  %-38s  %-25s %s" % (\
141       Attrs[1]['host'][0], \
142       Attrs[1]['architecture'][0], \
143       Attrs[1]['distribution'][0], \
144       Attrs[1]['access'][0], \
145       Attrs[1]['status'][0])
146
147 # Change a single attribute
148 def ChangeAttr(Attrs,Attr):
149    if (Attr in ["sponsor", "sshRSAHostKey", "purpose", "allowedGroups", "exportOptions", "ipHostNumber", "mXRecord", "sshdistAuthKeysHost"]):
150       return MultiChangeAttr(Attrs,Attr)
151
152    print "Old value: '%s'" % (GetAttr(Attrs,Attr,""))
153    print "Press enter to leave unchanged and a single space to set to empty"
154    NewValue = raw_input("New? ")
155
156    # Empty string
157    if (NewValue == ""):
158       print "Leaving unchanged."
159       return
160
161    # Single space designates delete, trap the delete error
162    if (NewValue == " "):
163       print "Deleting.",
164       try:
165          l.modify_s(HostDn,[(ldap.MOD_DELETE,Attr,None)])
166       except ldap.NO_SUCH_ATTRIBUTE:
167          pass
168
169       print
170       Attrs[1][Attr] = [""]
171       return
172
173    # Set a new value
174    print "Setting.",
175    l.modify_s(HostDn,[(ldap.MOD_REPLACE,Attr,NewValue)])
176    Attrs[1][Attr] = [NewValue]
177    print
178
179 def MultiChangeAttr(Attrs,Attr):
180    # Make sure that we have an entry
181    if not Attrs[1].has_key(Attr):
182       Attrs[1][Attr] = []
183
184    Attrs[1][Attr].sort()
185    print "Old values: ",Attrs[1][Attr]
186
187    Mode = raw_input("[D]elete or [A]dd? ").upper()
188    if (Mode != 'D' and Mode != 'A'):
189       return
190
191    NewValue = raw_input("Value? ")
192    # Empty string
193    if (NewValue == ""):
194       print "Leaving unchanged."
195       return
196
197    # Delete
198    if (Mode == "D"):
199       print "Deleting.",
200       try:
201          l.modify_s(HostDn,[(ldap.MOD_DELETE,Attr,NewValue)])
202       except ldap.NO_SUCH_ATTRIBUTE:
203          print "Failed"
204
205       print
206       Attrs[1][Attr].remove(NewValue)
207       return
208
209    # Set a new value
210    print "Setting.",
211    l.modify_s(HostDn,[(ldap.MOD_ADD,Attr,NewValue)])
212    Attrs[1][Attr].append(NewValue)
213    print
214
215
216 # Main program starts here
217 User = pwd.getpwuid(os.getuid())[0]
218 BindUser = User
219 ListMode = 0
220 FingerPrints = 0
221 Host = None
222 # Process options
223 try:
224    (options, arguments) = getopt.getopt(sys.argv[1:], "nh:a:rlf")
225 except getopt.GetoptError, data:
226    print data
227    sys.exit(1)
228
229 for (switch, val) in options:
230    if (switch == '-h'):
231       Host = val
232    elif (switch == '-a'):
233       BindUser = val
234    elif (switch == '-r'):
235       RootMode = 1
236    elif (switch == '-n'):
237       BindUser = ""
238    elif (switch == '-l'):
239       BindUser = ""
240       ListMode = 1
241    elif (switch == '-f'):
242       BindUser = ""
243       FingerPrints = 1
244
245 if (BindUser != ""):
246    l = passwdAccessLDAP(BaseDn, BindUser)
247 else:
248    l = connectLDAP()
249    l.simple_bind_s("","")
250
251 if ListMode == 1:
252    Attrs = l.search_s(HostBaseDn,ldap.SCOPE_ONELEVEL,"host=*")
253    hosts = []
254    for hAttrs in Attrs:
255       hosts.append(hAttrs[1]['host'][0])
256    hosts.sort()
257
258    print "%-12s  %-10s  %-38s  %-25s %s" % ("Host name","Arch","Distribution","Access","Status")
259    print "-"*115
260    for host in hosts:
261       for hAttrs in Attrs:
262          if host == hAttrs[1]['host'][0]:
263             Overview(hAttrs)
264    sys.exit(0)
265 elif FingerPrints == 1:
266    if Host is not None:
267       Attrs = l.search_s(HostBaseDn,ldap.SCOPE_ONELEVEL,"host=" + Host)
268    else:
269       Attrs = l.search_s(HostBaseDn,ldap.SCOPE_ONELEVEL,"host=*")
270    hosts = []
271    for hAttrs in Attrs:
272       hosts.append(hAttrs[1]['host'][0])
273    hosts.sort()
274
275    for host in hosts:
276       for hAttrs in Attrs:
277          if host == hAttrs[1]['host'][0]:
278             if 'sshRSAHostKey' in hAttrs[1].keys():
279                for key in hAttrs[1]['sshRSAHostKey']:
280                   keygen = subprocess.Popen(['/usr/bin/ssh-keygen', '-l', '-f', '-'],
281                                             stdin=subprocess.PIPE, stdout=subprocess.PIPE)
282                   keygen.stdin.write(key + '\n')
283                   keygen.stdin.close()
284                   input = keygen.stdout.readline()
285                   keygen.wait()
286                   fingerprint = input.split(' ')
287                   print "%s %s root@%s" % (fingerprint[0], fingerprint[1], host)
288    sys.exit(0)
289
290 HostDn = "host=" + Host + "," + HostBaseDn
291
292 # Query the server for all of the attributes
293 Attrs = l.search_s(HostBaseDn,ldap.SCOPE_ONELEVEL,"host=" + Host)
294 if len(Attrs) == 0:
295    print "Host",Host,"was not found."
296    sys.exit(0)
297
298 # repeatedly show the account configuration
299 while(1):
300    ShowAttrs(Attrs[0])
301    if (BindUser == ""):
302       sys.exit(0)
303
304    if RootMode == 1:
305       print "   a) Arbitary Change"
306    print "   n) New Host"
307    print "   d) Delete Host"
308    print "   u) Switch Hosts"
309    print "   x) Exit"
310
311    # Prompt
312    Response = raw_input("Change? ")
313    if (Response == "x" or Response == "X" or Response == "q" or
314        Response == "quit" or Response == "exit"):
315       break
316
317    # Change who we are looking at
318    if (Response == 'u' or Response == 'U'):
319       NewHost = raw_input("Host? ")
320       if NewHost == "":
321          continue
322       NAttrs = l.search_s(HostBaseDn,ldap.SCOPE_ONELEVEL,"host=" + NewHost)
323       if len(NAttrs) == 0:
324          print "Host",NewHost,"was not found."
325          continue
326       Attrs = NAttrs
327       Host = NewHost
328       HostDn = "host=" + Host + "," + HostBaseDn
329       OrderedIndex = copy.deepcopy(OrigOrderedIndex)
330       continue
331
332    # Create a new entry and change to it Change who we are looking at
333    if (Response == 'n' or Response == 'N'):
334       NewHost = raw_input("Host? ")
335       if NewHost == "":
336          continue
337       NAttrs = l.search_s(HostBaseDn,ldap.SCOPE_ONELEVEL,"host=" + NewHost)
338       if len(NAttrs) != 0:
339          print "Host",NewHost,"already exists."
340          continue
341       NewHostName = raw_input("Hostname? ")
342       if NewHost == "":
343          continue
344       Dn = "host=" + NewHost + "," + HostBaseDn
345       l.add_s(Dn,[("host", NewHost),
346                   ("hostname", NewHostName),
347                   ("objectClass", ("top", "debianServer"))])
348
349       # Switch
350       NAttrs = l.search_s(HostBaseDn,ldap.SCOPE_ONELEVEL,"host=" + NewHost)
351       if len(NAttrs) == 0:
352          print "Host",NewHost,"was not found."
353          continue
354       Attrs = NAttrs
355       Host = NewHost
356       HostDn = "host=" + Host + "," + HostBaseDn
357       OrderedIndex = copy.deepcopy(OrigOrderedIndex)
358       continue
359
360    # Handle changing an arbitary value
361    if (Response == "a"):
362       Attr = raw_input("Attr? ")
363       ChangeAttr(Attrs[0],Attr)
364       continue
365
366    if (Response == 'd'):
367       Really = raw_input("Really (type yes)? ")
368       if Really != 'yes':
369           continue
370       print "Deleting",HostDn
371       l.delete_s(HostDn)
372       continue
373
374    # Convert the integer response
375    try:
376       ID = int(Response)
377       if (not OrderedIndex.has_key(ID) or (ID > 100 and RootMode == 0)):
378          raise ValueError
379    except ValueError:
380       print "Invalid"
381       continue
382
383    # Print the what to do prompt
384    print "Changing LDAP entry '%s' (%s)" % (OrderedIndex[ID][0],OrderedIndex[ID][2])
385    print AttrPrompt[OrderedIndex[ID][2]][0]
386    ChangeAttr(Attrs[0],OrderedIndex[ID][2])