#!/usr/bin/python2.7
#
# Univention Configuration Registry
"""
List modified and not updated UCR templates.
"""
#
# Copyright 2011-2014 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 hashlib import md5
from optparse import OptionParser
from textwrap import dedent

PREFIX = '/etc/univention/templates/files'
SUFFIX = ('.dpkg-new', '.dpkg-dist')
K64 = 1 << 16


def main():
	opt = parse_cmdline()
	modified = set()
	if opt.dpkg:
		modified |= check_find(opt.verbose)
	if opt.md5:
		modified |= check_md5(opt.verbose)
	if modified:
		print >> sys.stderr, dedent("""\
			WARNING: The following UCR templates are modified locally.
			Updated versions will be named FILENAME.dpkg-*.
			The files should be checked for differences.
			""")
		print '\n'.join(sorted(modified))
		return 1
	return 0


def parse_cmdline():
	usage = "%prog [options]"
	description = sys.modules[__name__].__doc__
	parser = OptionParser(usage=usage, description=description)
	parser.add_option(
		'--md5',
		action='store_false', default=True,
		help='Disable checking MD5 sums.')
	parser.add_option(
		'--dpkg',
		action='store_false', default=True,
		help='Disable checking for renamed files.')
	parser.add_option(
		'--verbose', '-v',
		action='store_true',
		help='Enable verbose output.')
	opt, _args = parser.parse_args()
	return opt


def check_find(verbose=False):
	modified = set()
	for dirpath, dirnames, filenames in os.walk(PREFIX):
		for filename in filenames:
			for suffix in SUFFIX:
				if filename.endswith(suffix):
					filepath = os.path.join(dirpath, filename)
					if verbose:
						print >> sys.stderr, filepath
					basepath = filepath[:-len(suffix)]
					modified.add(basepath)
	return modified


def check_md5(verbose=False):
	modified = set()
	original = set()
	try:
		for filepath, expected in iter_templates():
			if filepath in original or filepath in modified:
				continue

			current = md5sum(filepath)
			if verbose:
				print >> sys.stderr, "%s %s %s" % (filepath, expected, current)
			if expected == current:
				original.add(filepath)
			else:
				modified.add(filepath)
		return modified
	except IOError as ex:
		print >> sys.stderr, ex
		sys.exit(2)


try:
	from debian.deb822 import Deb822

	def iter_templates():
		with open('/var/lib/dpkg/status', 'r') as dpkg_status:
			for pkg in Deb822.iter_paragraphs(dpkg_status, ["Conffiles"]):
				try:
					conffiles = pkg["Conffiles"]
				except KeyError:
					continue
				for conffile in conffiles.splitlines():
					fields = [_.strip() for _ in conffile.rsplit(' ', 1) if _]
					# skip obsolete and new conffiles
					if not fields or 'newconffile' in fields or 'obsolete' in fields:
						continue
					filepath, fmd5 = fields
					if filepath.startswith(PREFIX):
						yield filepath, fmd5
except ImportError:
	def iter_templates():
		prefix = " " + PREFIX
		with open('/var/lib/dpkg/status', 'r') as dpkg_status:
			for line in dpkg_status:
				if line.startswith(prefix):
					line = line.strip()
					fields = [_.strip() for _ in line.rsplit(' ', 1)]
					# skip obsolete and new conffiles
					if not fields or 'newconffile' in fields or 'obsolete' in fields:
						continue
					filepath, fmd5 = fields
					yield filepath, fmd5


def md5sum(filepath):
	digest = md5()
	try:
		with open(filepath, 'rb') as stream:
			while True:
				buf = stream.read(65536)
				if not buf:
					break
				digest.update(buf)
	except IOError:
		return None

	return digest.hexdigest()


if __name__ == '__main__':
	sys.exit(main())
