#!/usr/bin/python
#
# Copyright 2015 Univention GmbH
#
# http://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <http://www.gnu.org/licenses/>.

import os
import sys
from optparse import OptionParser
import traceback
import datetime

import univention.admin
import univention.admin.uldap
import univention.admin.config
import univention.admin.modules
import univention.admin.allocators as ualloc
from univention.config_registry import ConfigRegistry, handler_set, handler_unset


def collect_gids():
	users = dict()
	for dn, attrs in lo.search('(&(uidNumber=*)(objectClass=posixAccount))', attr=('uidNumber', )):
		xid = attrs.get('uidNumber')
		if xid:
			users[(int(xid[0]))] = dn
	groups = dict()
	for dn, attrs in lo.search('(&(gidNumber=*)(objectClass=posixGroup))', attr=('gidNumber', )):
		xid = attrs.get('gidNumber')
		if xid:
			groups[(int(xid[0]))] = dn

	common_ids = set(users.keys()).intersection(set(groups.keys()))

	if options.verbose:
		print "# uidNumbers: %r" % users.keys()
		print "# gidNumbers: %r" % groups.keys()
		print "# common ids: %r" % common_ids

	res = list()
	for cid in common_ids:
		new_gidNumber = int(cid)
		while new_gidNumber in groups or new_gidNumber in users:
			new_gidNumber = int(ualloc.request(lo, position, 'gidNumber'))
		res.append((cid, new_gidNumber))
	return res

def write_gids(gids, filename):
	with open(filename, "w") as f:
		f.writelines(["%d %d\n" % gid for gid in gids])

def read_gids(filename):
	res = list()
	with open(filename, "r") as f:
		for line in f:
			try:
				from_gid, to_gid = line.strip().split()
				res.append((int(from_gid), int(to_gid)))
				if options.verbose:
					print "# %r -> %r" % (from_gid, to_gid)
			except IndexError:
				pass
			except ValueError:
				pass
	return res

def change_gids(gids):
	for from_gid, to_gid in gids:
		try:
			group = univention.admin.modules.lookup(group_module, co, lo, scope='sub', superordinate=None, filter='(gidNumber=%d)' % from_gid)
			if len(group) == 1:
				group = group[0]
				if options.verbose:
					print "# %r -> %r (%r)" % (from_gid, to_gid, group['name'])
				group.descriptions["gidNumber"].may_change = True
				group['gidNumber'] = str(to_gid)
				group.descriptions["gidNumber"].may_change = False
				group.modify()
		except:
			print "Error changing %r -> %r, writing traceback to %s." % (from_gid, to_gid, err_file_name)
			err_file.write("GRPSET %r -> %r\n" % (from_gid, to_gid))
			err_file.write(traceback.format_exc())
			err_file.write("\n")

def walk_error(oserror):
	print "Error reading directory, writing traceback to %s." % err_file_name
	err_file.write("LIST %r\n" % oserror)

def chgrp(filepath, gid):
	uid = os.stat(filepath).st_uid
	os.chown(filepath, uid, gid)


parser = OptionParser()
parser.add_option("-d", "--dir", dest="chgrp_directory",
				  help="run chgrp recursivly on directory, expects change information from file with -i",
				  metavar="FILE")
parser.add_option("-i", "--infile", dest="in_filename", help="read group changes from FILE", metavar="FILE")
parser.add_option("-o", "--outfile", dest="out_filename", help="write group changes to FILE", metavar="FILE")
parser.add_option("-l", "--ldap", action="store_true", help="Modify GIDs of groups in LDAP, expects change information from file with -i [default=off].")
parser.add_option("-v", "--verbose", action="store_false", dest="verbose", default=True,
                  help="write about current action [default=on].")

(options, args) = parser.parse_args()

if options.chgrp_directory and not options.in_filename:
	parser.error("Please supply the directory that run chgrp should be run on recursivly with '-i'.")
if options.in_filename and options.out_filename:
	parser.error("Options -i and -o are mutually exclusive.")
if not options.in_filename and not options.out_filename:
	parser.error("Please use either -i or -o.")
if options.in_filename and not (options.chgrp_directory or options.ldap):
	parser.error("Please use -i with -d or -l.")
if options.ldap and not options.in_filename:
	parser.error("Please supply data to change the group information in LDAP with '-i'.")
if options.ldap and options.out_filename:
	parser.error("Options -l and -o are mutually exclusive.")

univention.admin.modules.update()
univention.admin.syntax.update_choices()
lo, position = univention.admin.uldap.getAdminConnection()
co = univention.admin.config.config()
group_module = univention.admin.modules.get('groups/group')

try:
	os.mkdir("/var/log/changegrp/")
except OSError:
	pass
err_file_name = "/var/log/changegrp/%s.log" % datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S")
err_file = open(err_file_name, "ab")

if options.out_filename:
	gids = collect_gids()
	write_gids(gids, options.out_filename)
	print "Wrote %d GID pairs to %s." % (len(gids), options.out_filename)
	err_file.close()
	sys.exit(0)

if options.in_filename:
	print "Reading list of groups from %r..." % options.in_filename

	gids = read_gids(options.in_filename)
	print "... read %d GID pairs." % len(gids)

	if options.ldap:
		print "Changing %d gidNumbers in LDAP..." % len(gids)
		change_gids(gids)
		print "... done."
		err_file.close()
		sys.exit(0)

	if options.chgrp_directory:
		gids_dict = dict(gids)
		stats = {"dir": 0, "file": 0}
		print "Running chgrp recursivly on %r..." % options.chgrp_directory
		for root, dirs, files in os.walk(options.chgrp_directory, onerror=walk_error):
			print "#  %r..." % root
			for _dir in dirs:
				dir_gid = os.stat(os.path.join(root, _dir)).st_gid
				if dir_gid in gids_dict:
					stats["dir"] += 1
					chgrp(os.path.join(root, _dir), gids_dict[dir_gid])
					if options.verbose:
						err_file.write("CHGRP %r: %r -> %r\n" % (os.path.join(root, _dir), dir_gid, gids_dict[dir_gid]))
			for _file in files:
				file_gid = os.stat(os.path.join(root, _file)).st_gid
				if file_gid in gids_dict:
					stats["file"] += 1
					chgrp(os.path.join(root, _file), gids_dict[file_gid])
					if options.verbose:
						err_file.write("CHGRP %r: %r -> %r\n" % (os.path.join(root, _file), file_gid, gids_dict[file_gid]))
		print "... done. Changed permissions on %d directories and %d files." % (stats["dir"], stats["file"])
		err_file.close()
		sys.exit(0)
