#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# univention-support-info - collect system information
#
# 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/>.

REPOSITORY_COMPONENT = 'support'
ONLINE_REPOSITORY_URL = 'http://apt.software-univention.de/'
WARN_DAYS = 30
LOGFILE_NAME='/var/log/univention/univention-support-info.log'

keyID = '18B913ABF3C1550B8C2DFDA994F01970E89E3BEC'
# create with: gpg --armor --export --export-options export-minimal $keyID
keyData = '\n'.join(('-----BEGIN PGP PUBLIC KEY BLOCK-----',
					 'Version: GnuPG v1.4.9 (GNU/Linux)',
					 '',
					 'mQGiBEA2RhgRBADee7hGh5TPPwqs+z5tfLett9xaVxmRkUfBvqesVIj93WzrKsU3',
					 'S7hjnMlh+JieFjJLy6jTJYtvqqFIxh3vw5btCwhntrTQnu00U/r3UdznqE/zGH2L',
					 'A734aHaSaq6UFKE5kwX0DECgSI1hwc20d7guLJXSqOpwfYktXiB+27GRCwCgh4OR',
					 'MWncPkhaJhusO8YCSnSN0GED/2ez8utubP1FloTfTof4/OLfBvPwdgJ5Q7FRqeF9',
					 'wDmfd8Hetzr+Fh4zMs6dY0c5+unUQiLXjY9F01WT7SM+yFrs5EHzb+gyjIdTmTtn',
					 'mtNTL2cZK8freAv9LPWCHfQ1rii+Qd71+/CKLDfwwLqQxEAkOsrpOsUD4dip6vkm',
					 'ZtaRBACUYoOtB738OzjPqOpbnmNQjQYVtGCocDjfKs+bMyq+LPOyC31NVC4LvqhC',
					 'nwXSUSy8jfXLzInPXgEUiHvGAEvnNzRLAIh6W/pIaK7tIITESmV/C3PBvxLEkJNm',
					 '8Ll+2qGVspYnGTUFe6JzxARzcTVow4JlB+dX40bk7LNqUe6Au7QqVW5pdmVudGlv',
					 'biBTdXBwb3J0IDxzdXBwb3J0QHVuaXZlbnRpb24uZGU+iF4EExECAB4GCwkIBwMC',
					 'AxUCAwMWAgECHgECF4AFAkm+NOYCGQEACgkQlPAZcOieO+xhXwCfdZ7+eWGpJfhz',
					 'CnrfuzgdzqsetMMAn3oUbDZCqzH083DKCNS5547V8XkCtCpVbml2ZW50aW9uIFN1',
					 'cHBvcnQgPHNlcnZpY2VAdW5pdmVudGlvbi5kZT6IYAQTEQIAIAUCSNdW8wIbIwYL',
					 'CQgHAwIEFQIIAwQWAgMBAh4BAheAAAoJEJTwGXDonjvsQcoAn2b9FNPECtPus0Sf',
					 '3ENGafyXSIq7AJ9sbNm+QryXW1rtvtsHlNgYI9eGpLQrVW5pdmVudGlvbiBTdXBw',
					 'b3J0IDxmZWVkYmFja0B1bml2ZW50aW9uLmRlPohgBBMRAgAgBQJI11aYAhsjBgsJ',
					 'CAcDAgQVAggDBBYCAwECHgECF4AACgkQlPAZcOieO+wmzACfVmZKVc37T7W9FQGN',
					 'K+dXsLbDplkAniAYh0l9Nd8wMh/QZrxcRdlgOBrhtDRVbml2ZW50aW9uIFN1cHBv',
					 'cnQgPHBhcnRuZXItdGVjaG5pY2FsQHVuaXZlbnRpb24uZGU+iGAEExECACAFAkho',
					 'p9gCGyMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRCU8Blw6J477IMYAJ9oZoTS',
					 'vHsxF7TYJSGo3pcutuV4RACfbxLxxjU7suCGs+khvINRl9pJel60KlVuaXZlbnRp',
					 'b24gU2VydmljZSA8c2VydmljZUB1bml2ZW50aW9uLmRlPohgBBMRAgAgBQJJvjVV',
					 'AhsjBgsJCAcDAgQVAggDBBYCAwECHgECF4AACgkQlPAZcOieO+ycGwCeM8pJxUrL',
					 'oiKylTxiKZ4MwQoV/L0An3y/9GafqT8SaUgSjYO+9P4szWATtCtQYXJ0bmVyLUxp',
					 'c3RzIDxwYXJ0bmVyLWxpc3RzQHVuaXZlbnRpb24uZGU+iGAEExECACAFAkm+NQEC',
					 'GyMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRCU8Blw6J477C67AJ93Pv9Dcq7g',
					 'HreBv3f1Q07IjDI3bACfUZgClXNISIewXER8FzV5cC08LRm0LFVuaXZlbnRpb24g',
					 'RmVlZGJhY2sgPGZlZWRiYWNrQHVuaXZlbnRpb24uZGU+iGAEExECACAFAkm+NSsC',
					 'GyMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRCU8Blw6J477E8QAJ0XE91GawgL',
					 'lVGfeiiT7c32P35IDACePCNLSzHA6K6JrV7CP98BeZUUNYu0LFVuaXZlbnRpb24g',
					 'SGVscGRlc2sgPGhlbHBkZXNrQHVuaXZlbnRpb24uZGU+iGAEExECACAFAkm+NUMC',
					 'GyMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRCU8Blw6J477JNxAJ4l9wqf+WtV',
					 'C0sAtu91aGWvgW/JQACfTJUCWf1DTqn1sDhbNiCG2jA+B9K0LFVuaXZlbnRpb24g',
					 'VmVydHJpZWIgPHZlcnRyaWViQHVuaXZlbnRpb24uZGU+iGAEExECACAFAkm+NWwC',
					 'GyMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRCU8Blw6J477Jq3AJ9fN/6oKbqY',
					 'exKHPdLQw31wC8HGTgCfZ5gn5zDd2JSY9tirDtMw5SSdfk20M1BhcnRuZXItVGVj',
					 'aG5pY2FsIDxwYXJ0bmVyLXRlY2huaWNhbEB1bml2ZW50aW9uLmRlPohgBBMRAgAg',
					 'BQJJvjUPAhsjBgsJCAcDAgQVAggDBBYCAwECHgECF4AACgkQlPAZcOieO+wlNQCf',
					 'eZ7Z4PeCx7SF+k1CK0bC8oouICkAn3/NYT1ASQLFP8CBqku1shs1lFCstDNQcmVt',
					 'aXVtLVRlY2huaWNhbCA8cHJlbWl1bS10ZWNobmljYWxAdW5pdmVudGlvbi5kZT6I',
					 'YAQTEQIAIAUCSb41GgIbIwYLCQgHAwIEFQIIAwQWAgMBAh4BAheAAAoJEJTwGXDo',
					 'njvsxqcAnjRmgwWtXJqEbptTSSkaamNmGp9KAJ0R7b4RYDSKr+aFGPhhgetqLNXO',
					 'MrQwVW5pdmVudGlvbiBPWCBTdXBwb3J0IDxveC1zdXBwb3J0QHVuaXZlbnRpb24u',
					 'ZGU+iGAEExECACAFAk1GmXcCGyMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRCU',
					 '8Blw6J477JwmAJ9WeeR2SzUtHY7pPolhm4fQmtM20QCfaebVO97CHdKkcAqPjJ3K',
					 'RwWVtpC5AQ0EQDZGGxAEAJotyyct6jvNscl9q2stDB+BUuXefdd7UMdSySSGqt5c',
					 '7f/6IEX/eiG/2nIsqX1IsSQ+Bw0ZQTQUhgI8hICUsmjdjvWnBCyHX8xnMESITDv/',
					 'fJmxgaP8fbMSJexhnizjlz8m74OgnfFew6EuRWRXA/SDeTwmsaUafTv7biKaDlU7',
					 'AAMFA/9hJUqdh+tSaEfwUzPgHdFT8EIM2B0VSmVnqHSWwCjuJTLTWJi+DDe2hq7p',
					 'QPpcATzgEg5qu5lsqh0AAXV998fD/RiO3B+ct1rwYbNlchACIXtgDTe43dmUaKkp',
					 'fPRxeQZr8iym706LJOyppF+jXqOm2oy6Sf++/YElcCBmPPDIwIhGBBgRAgAGBQJA',
					 'NkYbAAoJEJTwGXDonjvsqD0AnRVxlYyWk3DrKL0ZCxRZrtpW6pbwAJ9R/HZLaaH+',
					 '043H7VXVPPTjhs6Tig==',
					 '=BE0y',
					 '-----END PGP PUBLIC KEY BLOCK-----',
					 ))
toprc = '\n'.join(('RCfile for "top with windows"		   # shameless braggin\'',
				   'Id:a, Mode_altscr=0, Mode_irixps=1, Delay_time=3.000, Curwin=0',
				   'Def	 fieldscur=AEHIOQTWKNMbcdfgjplrsuvyzX',
				   '		winflags=62905, sortindx=10, maxtasks=0',
				   '		summclr=1, msgsclr=1, headclr=3, taskclr=1',
				   'Job	 fieldscur=ABcefgjlrstuvyzMKNHIWOPQDX',
				   '		winflags=62777, sortindx=0, maxtasks=0',
				   '		summclr=6, msgsclr=6, headclr=7, taskclr=6',
				   'Mem	 fieldscur=ANOPQRSTUVbcdefgjlmyzWHIKX',
				   '		winflags=62777, sortindx=13, maxtasks=0',
				   '		summclr=5, msgsclr=5, headclr=4, taskclr=5',
				   'Usr	 fieldscur=ABDECGfhijlopqrstuvyzMKNWX',
				   '		winflags=62777, sortindx=4, maxtasks=0',
				   '		summclr=3, msgsclr=3, headclr=2, taskclr=3',
				   '',
				   ))

import cStringIO
import glob
import gzip
import optparse
import os
import shutil
import socket
import stat
import subprocess
import sys
import tarfile
import tempfile
import time
import re
from distutils.spawn import find_executable
import logging

# ignore apt's "API not stable yet" warning
import warnings
warnings.filterwarnings("ignore", category=FutureWarning, append=True)

import apt
import apt_pkg
import grp

try:
	from univention import config_registry
except ImportError:
	if os.path.isfile('/usr/share/pyshared/univention/config_registry.py'):
		import imp
		config_registry = imp.load_source('config_registry', '/usr/share/pyshared/univention/config_registry.py')
	else:
		sys.path.append('/usr/share/pyshared')
		from univention import config_registry

ucr = config_registry.ConfigRegistry()
ucr.load()
timeString = time.strftime('%Y-%m-%d_%H-%M-%SZ', time.gmtime())
hostname = socket.gethostname()



def Popen(CommandTuple, Input=None):
	try:
		process = subprocess.Popen(CommandTuple, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True, env=env)
		(stdoutdata, stderrdata) = process.communicate(input=Input)
		return (process.returncode, stdoutdata, stderrdata)
	except (OSError, IOError), error:
		return ('Could not execute %s: %s' % (repr(CommandTuple), repr(' '.join(map(str, error.args)))), '', '')

def _ldapsearchCommand():
	''' Get ldapsearch command depending on UCS version '''
	if find_executable('univention-ldapsearch'):
		return 'univention-ldapsearch'
	return 'ldapsearch'

def _sprint(string):
	''' write status string to stdout without newline '''
	sys.stdout.write(string)
	sys.stdout.flush()

def U32(i):
	'''
	Return i as an unsigned integer, assuming it fits in 32 bits.
	If it's >= 2GB when viewed as a 32-bit unsigned int, return a long.
	'''
	if i < 0:
		i += 1L << 32
	return i

def addFile(name, size, fileobj):
	# try to determine filesize
	if size == None or size == False:
		fileobj.seek(0,os.SEEK_END)
		size = fileobj.tell()
	if fileobj.tell() > 0:
		fileobj.seek(0)

	info = tarfile.TarInfo()
	info.size = size
	info.name = 'univention-support-info-' + hostname + '-' + timeString + '/' + name
	info.mode = stat.S_IRUSR
	info.mtime = time.time()
	archive.addfile(info, fileobj)

def addFileByPath(filename):
	if filename.startswith('/proc/'):
		# This is a /proc/ file, os.stat(filename).st_size will always return 0
		tmpf = tempfile.mkstemp(prefix='univention-support-info.')[1]
		try:
			shutil.copy(filename, tmpf)
		except (OSError, IOError), error:
			error = '\n'.join(map(str, error.args))
			addFile('files/' + filename.strip('/').replace('/', '_') + '.ERROR', len(error), cStringIO.StringIO(error))
			os.unlink(tmpf)
		filesize = os.stat(tmpf).st_size
		os.unlink(tmpf)
	else:
		try:
			filesize = os.stat(filename).st_size
		except (OSError, IOError), error:
			filesize = 0

	try:
		fileobj = open(filename, 'rb')
	except (OSError, IOError), error:
		error = '\n'.join(map(str, error.args))
		addFile('files/' + filename.strip('/').replace('/', '_') + '.ERROR', len(error), cStringIO.StringIO(error))
	else:
		addFile('files/' + filename.strip('/').replace('/', '_'), filesize, fileobj)
		fileobj.close()

def certificateValidity(CertificatePath):
	try:
		import M2Crypto
	except ImportError, error:
		error = '\n'.join(map(str, error.args))
		addFile('info/ssl/' + CertificatePath.strip('/').replace('/', '_') + '.ERROR', len(error), cStringIO.StringIO(error))
		return

	try:
		cert = M2Crypto.X509.load_cert(CertificatePath)
		validity = '%s\nNot Before: %s\nNot After : %s\n' % (CertificatePath, cert.get_not_before(), cert.get_not_after(), )
		addFile('info/ssl/' + CertificatePath.strip('/').replace('/', '_'), len(validity), cStringIO.StringIO(validity))
	except (OSError, IOError, M2Crypto.X509.X509Error), error:
		error = '\n'.join(map(str, error.args))
		addFile('info/ssl/' + CertificatePath.strip('/').replace('/', '_') + '.ERROR', len(error), cStringIO.StringIO(error))

def certificateValidities():
	_sprint( 'Checking certificate validity: ' )
	CertificatePatterns = [
		'/etc/univention/ssl/*.*/cert.pem',
		'/etc/univention/ssl/ucsCA/CAcert.pem',
		]
	for CertificatePattern in CertificatePatterns:
		for CertificatePath in glob.glob(CertificatePattern):
			certificateValidity(CertificatePath)
	print 'done.'

def simpleFiles():
	FilePatterns = [
		'/boot/config-*',
		'/etc/apache2/*',
		'/etc/apache2/*/*',
		'/etc/apt/sources.list',
		'/etc/apt/sources.list.d/*',
		'/etc/apt/mirror.list',
		'/etc/cron*/*',
		'/etc/fstab',
		'/etc/imapd/*',
		'/etc/mtab',
		'/etc/passwd',
		'/etc/procmailrc',
		'/etc/spamassassin/*',
		'/etc/univention/connector/ad/mapping.py',
		'/etc/univention/installation_profile',
		'/etc/ox-secrets/*',
		'/opt/open-xchange/etc/*',
		'/opt/open-xchange/etc/*/*',
		'/opt/open-xchange/etc/*/*/*',
		'/proc/mounts*',
		'/proc/cmdline',
		'/var/lib/univention-directory-replication/failed.ldif',
		'/etc/postfix/*',
		'/etc/imapd/*',
		'/var/univention-backup/ad-takeover/*',
		'/etc/*/local.conf*',
		]
	FileExcludePatterns = [ '/etc/apache2/mods-available/*', '/etc/apache2/sites-available/*' ]

	Files = set()
	ExcludeFiles = list()
	for FileExcludePattern in FileExcludePatterns:
		ExcludeFiles.extend(glob.glob(FileExcludePattern))
	for FilePattern in FilePatterns:
		for Filename in glob.glob(FilePattern):
			if not Filename in ExcludeFiles:
				Files.add(Filename)

	_sprint( 'Collecting files: ' )
	for Filename in sorted(list(Files)):
		if os.path.isfile(Filename):
			addFileByPath(Filename)
			_sprint('.')
	print 'done.'

def licenseObject():
	''' Get license object from ldap (cn=license) '''	
	stdout = executeCommand( 'univention-license-object', (_ldapsearchCommand(), '-x' , '-b', 'cn=license,cn=univention,'+ucr.get('ldap/base')) )
	addFile('info/univention-license-object', len(stdout), cStringIO.StringIO(stdout))

	
def checkMaintenance():
	''' Check if UCS-Version is in maintenance '''
	if ucr.get('version/version') <= '2.3':
		tmpf = tempfile.TemporaryFile(prefix='univention-support-info.')
		print "Please note, system is no longer maintained, security updates are no longer available for current UCS Version %s" % (ucr.get('version/version'))
		print >> tmpf, "Please note, system is no longer maintained, security updates are no longer available for current UCS Version %s" % (ucr.get('version/version'))
		addFile( 'info/no_maintenance', None, tmpf )

def aptPackageList():
	"""List installed packages and their source repository."""
	_sprint( 'Collecting package lists: ' )
	cache = apt.Cache()

	packagesAll = tempfile.TemporaryFile(prefix='univention-support-info.')
	packagesUnknownSource = tempfile.TemporaryFile(prefix='univention-support-info.')

	if not hasattr(apt, 'deprecation'):  # python apt 0.7.7 in UCS < 3.0
		packages = [_ for _ in cache if _.isInstalled]
		for pkg in packages:
			pkg._lookupRecord(True)
			try:
				path = apt_pkg.ParseSection(pkg._records.Record)["Filename"]
			except KeyError:
				print >> packagesUnknownSource, "%s\tUNKNOWN" % (pkg.name,)
				continue
			cand = pkg._depcache.GetCandidateVer(pkg._pkg)
			for packagefile, _ in cand.FileList:
				indexfile = cache._list.FindIndex(packagefile)
				if indexfile:
					uri = indexfile.ArchiveURI(path)
					print >> packagesAll, "%s\t%s" % (pkg.name, uri)
	else:
		packages = [_ for _ in cache if _.is_installed]
		for pkg in packages:
			version = pkg.installed.version
			package = pkg.versions[version]
			try:
				uri = package.uri
			except StopIteration:
				print >> packagesUnknownSource, "%s\tUNKNOWN" % (pkg.name,)
				continue
			print >> packagesAll, "%s\t%s" % (pkg.name, uri)
	
	addFile( 'info/packages_all', None, packagesAll )
	addFile( 'info/packages_unknown-source', None, packagesUnknownSource )
	print 'done.'

def executeCommand(commandName, command, log_stderr = False):
	(exitcode, stdout, stderr, ) = Popen(command)
	if exitcode or log_stderr:
		if type(exitcode) is int:
			stderr += '\nExitcode was %d\n' % exitcode
		else:
			stderr += exitcode + '\n'
		addFile('info/' + commandName + '.stderr', len(stderr), cStringIO.StringIO(stderr))
	return stdout

def templateFiles():
	_sprint( 'Searching for changed template files: ' )
	stdout = executeCommand('check-templates', ('find', '/etc/univention/templates/files/', '(', '-name', '*.dpkg-new', '-o', '-name', '*.dpkg-dist', ')', '-print0', ))
	files = [templatefile for templatefile in stdout.split('\0') if templatefile]
	message = ('Found %d:\n' % len(files)) + '\n'.join(files) + '\n'
	addFile('info/check-templates', len(message), cStringIO.StringIO(message))
	for templatefile in files:
		addFileByPath(templatefile)
		if templatefile.endswith('.dpkg-new'):
			addFileByPath(templatefile[:-len('.dpkg-new')])
		elif templatefile.endswith('.dpkg-dist'):
			addFileByPath(templatefile[:-len('.dpkg-dist')])
		_sprint('.')
	print 'done.'

def collectCommandData():
	commands = {'hostname-f':
					('hostname', '--fqdn', ),
				'ifconfig-a':
					('ifconfig', '-v', '-a', ),
				'iptables-L_filter':
					('iptables', '-L', '-n', '-v', '-t', 'filter', ),
				'iptables-L_nat':
					('iptables', '-L', '-n', '-v', '-t', 'nat', ),
				'iptables-L_mangle':
					('iptables', '-L', '-n', '-v', '-t', 'mangle', ),
				'iptables-L_raw':
					('iptables', '-L', '-n', '-v', '-t', 'raw', ),
				'iptables-save':
					('iptables-save', '-c', ),
				'route-4':
					('route', '-v', '-ee', '-n', '-A', 'inet', ),
				'route-6':
					('route', '-v', '-ee', '-n', '-A', 'inet6', ),
				'netstat':
					('netstat', '--tcp', '--udp', '--listening', '--program', '--extend', '--extend', '--verbose', '--timers', '--numeric', '--wide', ),
				'dpkg-l':
					('dpkg-query', '--show', '--showformat=${Status}\t${Package}\t${Version}\n', ),
				'dpkg--audit':
					('dpkg', '--audit', ),
				'uname':
					('uname', '-a', ),
				'ps':
					('ps', '-AHFly', ),
				'ps-full':
					('ps', '-ALwwo', 'stat,pid,ppid,sid,tty,nlwp,lwp,pri,ni,sched,wchan,vsz,rss,sz,pcpu,pmem,cmd,blocked,caught,ignored,pending,lstart,cls,time,flags,uid,user,ruid,ruser,suid,suser,gid,group,rgid,rgroup,sgid,sgroup', ),
				'ucr-dump':
					('univention-config-registry', 'dump', ),
				'df-full':
					('df', '--portability', '--print-type', ),
				'df-i-full':
					('df', '--portability', '--print-type', '--inodes', ),
				'df':
					('df', '-h', ),
				'df-i':
					('df', '-h', '-i', ),
				'join-status':
					('univention-check-join-status', ),
				'virsh-qemu':
					('virsh', '-c', 'qemu:///system', 'capabilities', ),
				'virsh-xen':
					('virsh', '-c', 'xen:///',		'capabilities', ),
				'top':
					('top', '-b', '-n2', ),
				'testparm':
					(('testparm', '-s', '-vvv', ), True),
				'listenerID':
					('cat', '/var/lib/univention-directory-listener/notifier_id', ),
				'notifierID':
					('/usr/share/univention-directory-listener/get_notifier_id.py', ),
				'mailq':
					('mailq', ),
		   		'univention-license-check':
					('univention-license-check', ),
				'hostaccount-id':
					('id', ucr.get('hostname') + '$', ),
				}

	# Commands depending on samba version
	if sambaDomainVersion == 3:
		commands.update({'test-join':
							('net', 'rpc', 'testjoin', ),	
						})
	elif sambaDomainVersion == 4:
		commands.update({'net-ads-info':
							('net', 'ads', 'info', ),
						'net-ads-lookup':
							('net', 'ads', 'lookup', ),
						})
		if ucr.get('samba4/role', None):
			# Run only S4
			commands.update({'samba-tool-drs-showrepl':
								('samba-tool', 'drs', 'showrepl', ),
							'samba-tool-domain-passwordsettings':
								('samba-tool', 'domain', 'passwordsettings', 'show' ),
							'testparm-samba4':
								(('testparm.samba4', '-s', '-vvv'), True),
							'sysvol-getfacl':
								('getfacl', '-R', '/var/lib/samba/sysvol'),
							'sysvol-ntacl':
								('find', '/var/lib/samba/sysvol', '-printf', '# file: %p\n', '-exec', find_executable('samba-tool'), 'ntacl', 'get', '--as-sddl', '{}', ';'),
							'samba-tool-fsmo-show':
								('samba-tool', 'fsmo', 'show'),
							'univention-s4connector-list-rejected':
								('univention-s4connector-list-rejected'),
							})
			# >= Samba4 RC (UCS 3.1)
			if ucr.get('version/version') >= '3.1':
				commands.update({'samba-tool-domain-info':
									('samba-tool', 'domain', 'info', '127.0.0.1', ),
								})
		else:
			# Run only on S3 member in S4 domain
			commands.update({'test-join':
								('net', 'ads', 'testjoin', ),
							})

	_sprint( 'Collecting command output: ' )
	for commandName in commands:
		command = commands[commandName]
		if type(command[0]) == tuple:
			stdout = executeCommand(commandName, command[0], command[1])
		else:
			stdout = executeCommand(commandName, command)

		addFile('info/' + commandName, len(stdout), cStringIO.StringIO(stdout))
		_sprint('.')
	print 'done.'

def univentionSystemInfo():
	_sprint( 'Collecting hardware information: ' )
	manu = executeCommand('dmidecode', ('dmidecode', '-s', 'system-manufacturer'))
	product = executeCommand('dmidecode', ('dmidecode', '-s', 'system-product-name'))
	if not manu:
		manu = 'Unknown'
	if not product:
		product = 'Unknown'
	stdout = executeCommand('univention-system-info', ('univention-system-info','-u','-m',manu,'-t',product,'-c','Created by univention-support-info','-s','-',))
	archive = None
	for line in stdout.split('\n'):
		if line.startswith('archive'):
			archive = line.split(':', 1)[1]
	if not archive:
		error = 'No archive returned!'
		error += '\nunivention-system-info stdout:\n%s' % stdout
		addFile('info/univention-system-info.ERROR', len(error), cStringIO.StringIO(error))
		return
	filename =os.path.join( '/var/www/univention-management-console/system-info/', archive )
	# If UMC is not installed /var/www/univention-management-console/system-info/ does not exist and archive stays in /tmp
	if not os.path.isfile( filename ):
		filename = os.path.join( '/tmp/', archive )
	try:
		archive = tarfile.open(name=filename, mode='r:*')
		for member in archive:
			if member.isfile():
				fileobj = archive.extractfile(member)
				addFile('info/univention-system-info/' + member.name, member.size, fileobj)
				fileobj.close()
		archive.close()
	except (IOError, tarfile.TarError, ), error:
		error = '\n'.join(map(str, error.args))
		error += '\nunivention-system-info stdout:\n%s' % stdout
		addFile('info/univention-system-info.ERROR', len(error), cStringIO.StringIO(error))
	print 'done.'

def rotatedLogs():
	DefaultMaxLineCount = 1000
	FilePatterns = [
		('/var/log/apache/*', DefaultMaxLineCount),
		('/var/log/apache2/*', DefaultMaxLineCount),
		('/var/log/auth.log*', DefaultMaxLineCount),
		('/var/log/dpkg.log*', DefaultMaxLineCount),
		('/var/log/mail.log*', DefaultMaxLineCount),
		('/var/log/open-xchange/*', DefaultMaxLineCount),
		('/var/log/samba/log.*', DefaultMaxLineCount),
		('/var/log/kern*', DefaultMaxLineCount),
		('/var/log/univention/*', DefaultMaxLineCount),
		('/var/log/apt/term.log*', DefaultMaxLineCount),
		('/var/log/squid/*', DefaultMaxLineCount),
		('/var/log/dansguardian/*', DefaultMaxLineCount),
		('/var/log/syslog*', 2000000),
		('/var/log/univention/system-stats.log*', 5000000),
		('/var/log/freeradius/*', DefaultMaxLineCount),
		('/var/log/cups/*', DefaultMaxLineCount),
		(LOGFILE_NAME, DefaultMaxLineCount),
		]
	FullLogs = set(( # for these every available log-file shall be included
			'/var/log/dpkg.log',
			'/var/log/univention/updater.log',
			'/var/log/apt/term.log',
			'/var/log/univention/ad-takeover.log',
			))
	GzipSuffix = '.gz'
	logs = {}
	for FilePattern in [fp[0] for fp in FilePatterns]:
		for filename in glob.glob(FilePattern):
			if os.stat(filename).st_size <= 0:
				# ignore 0 byte files
				continue
			if filename.endswith(GzipSuffix):
				gzipped = True
				filename = filename[:-len(GzipSuffix)]
			else:
				gzipped = False
			if not filename.endswith('.') and os.path.splitext(filename)[1].strip('0123456789') == '.': # has extension and only digits in it
				(filename, ext, ) = os.path.splitext(filename)
				number = int(ext.lstrip('.'))
			else:
				number = -1
			if filename not in logs:
				logs[filename] = {}
			logs[filename][number] = gzipped

	_sprint( 'Collecting logfiles: ' )
	for logname in sorted(logs):
		logLinecount = 0
		nonemptyNumber = 0
		for number in sorted(logs[logname]):
			# is logname gzipped?
			gzipped = logs[logname][number]
			path = logname
			fileLinecount = 0
			if number != -1:
				path += '.%d' % number
			
			if gzipped:
				try:
					logfile = open(path + GzipSuffix, 'rb')
				except (OSError, IOError, ), error:
					error = '\n'.join(map(str, error.args))
					addFile('files/' + '%s_%d.stderr' % (logname.strip('/').replace('/', '_'), number, ), len(error), cStringIO.StringIO(error))
					continue

				# calc bytes and linecount of logfile
				# last 4 bytes of a gzip file contain the size of the original (uncompressed) input data modulo 2^32.
				try:
					logfile.seek(-4, os.SEEK_END)
				except IOError, e:
					print '\n\nFilename: %s' %path + GzipSuffix
					raise IOError(e)
				fileBytes = U32( gzip.read32(logfile) )
				logfile.close()
				fileLinecount = int( subprocess.Popen( 'zcat -f %s | wc -l' % (path + GzipSuffix), stdout=subprocess.PIPE, shell=True).stdout.read().strip() )
			else:
				# addFile may calculate the size for us
				fileBytes = None
				fileLinecount = int( subprocess.Popen( ('wc', '-l', path), stdout=subprocess.PIPE).stdout.read().strip().split()[0] )
			logLinecount += fileLinecount
			#if gzipped:
			#	_sprint( '"%s", "%s", "%s" -> ' % (path + GzipSuffix, fileBytes, fileLinecount) )
			#else:
			#	_sprint( '"%s", "%s", "%s" -> ' % (path, fileBytes, fileLinecount) )

			if fileLinecount <= 0:
				# skip logname if empty
				#print 'ERROR: Empty file? "%s"' % path
				continue


			nonemptyNumber += 1
			try:
				if gzipped:
					logfile = gzip.GzipFile(path + GzipSuffix, 'rb')
				else:
					logfile = open(path, 'rb')
			except (OSError, IOError, ), error:
				error = '\n'.join(map(str, error.args))
				addFile('files/' + '%s_%d.stderr' % (logname.strip('/').replace('/', '_'), nonemptyNumber, ), len(error), cStringIO.StringIO(error))
				continue

			# Add file to archive ...
			addFile('files/' + '%s_%d' % (logname.strip('/').replace('/', '_'), nonemptyNumber, ), fileBytes, logfile)
			#print 'Added as "%s_%d"' % (logname.strip('/').replace('/', '_'), nonemptyNumber, )
			logfile.close()

			if logname not in FullLogs and logLinecount > filter(lambda x: logname.startswith(x[0].replace('*', '')), FilePatterns)[0][1]:
				break

			_sprint('.')
	print 'done.'

def atJobs():
	'''
	Generate a list of at-Jobs (usefull for UCS@school)
	'''
	try:
		from univention.lib import atjobs as at
	except ImportError, error:
		error = str(error.message)
		addFile('info/at-jobs' + '.ERROR', len(error), cStringIO.StringIO(error))
		return

	jobs = ''
	for job in at.list( extended=True ):
		jobs += '\n'.join( (str(job), job.command) )
	
	addFile('info/at-jobs', len(jobs), cStringIO.StringIO(jobs))


def tryDelete(filename):
	try:
		os.remove(filename)
	except (OSError, IOError, ):
		pass

def gpg(archiveFileName):
	_sprint( 'Encrypting file: ' )
	keyringFileName = tempfile.mkstemp(prefix='univention-support-info-keyring.', suffix='.gpg')[1]
	secringFileName = tempfile.mkstemp(prefix='univention-support-info-secring.', suffix='.gpg')[1]
	trustdbFileName = tempfile.mkstemp(prefix='univention-support-info-trustdb.', suffix='.gpg')[1]
	tryDelete(trustdbFileName) # HACK: file must not exist for gpg to work
	gpgFileName = archiveFileName + '.gpg'
	gpgBase = ('gpg',
			   '--batch', '--quiet', '--no-tty',
			   '--with-colons', '--utf8-strings',
			   '--no-auto-check-trustdb', '--no-auto-key-locate', '--no-use-agent',
			   '--no-options',
			   '--no-random-seed-file',
			   '--trust-model','always',
			   '--trustdb-name', trustdbFileName,
			   '--secret-keyring', secringFileName,
			   '--no-default-keyring', '--keyring', keyringFileName,
			   )
	gpgImport  = gpgBase + ('--import',
							)
	gpgEncrypt = gpgBase + ('--recipient', keyID,
							'--encrypt', archiveFileName,
							)
	(exitcode, stdout, stderr, ) = Popen(gpgImport, Input=keyData)
	if exitcode:
		print 'gpg-import failed with %s' % exitcode
		if stdout:
			print 'stdout: ' + repr(stdout)
		if stderr:
			print 'stderr: ' + repr(stderr)
		tryDelete(keyringFileName)
		tryDelete(keyringFileName + '~')
		tryDelete(secringFileName)
		tryDelete(trustdbFileName)
		tryDelete(gpgFileName)
		return
	(exitcode, stdout, stderr, ) = Popen(gpgEncrypt)
	if exitcode:
		print 'gpg-encrypt failed with %s' % exitcode
		if stdout:
			print 'stdout: ' + repr(stdout)
		if stderr:
			print 'stderr: ' + repr(stderr)
		tryDelete(keyringFileName)
		tryDelete(keyringFileName + '~')
		tryDelete(secringFileName)
		tryDelete(trustdbFileName)
		tryDelete(gpgFileName)
		return
	tryDelete(keyringFileName)
	tryDelete(keyringFileName + '~')
	tryDelete(secringFileName)
	tryDelete(trustdbFileName)
	os.chmod(gpgFileName, stat.S_IWUSR | stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)
	print 'done.'
	return gpgFileName

def checkForRoot():
	if os.geteuid() != 0:
		print 'Please run this program as root!'
		sys.exit(3)

def prepareArchive():
	global archiveFileName, archiveFile, archive
	archiveFileName = tempfile.mkstemp(prefix='univention-support-info-%s.' % hostname, suffix='.tar.bz2')[1]
	archiveFile = open(archiveFileName, 'wb')
	archive = tarfile.open(mode='w|bz2', fileobj=archiveFile)

def closeArchive():
	archive.close()
	archiveFile.close()

def prepareEnvironment():
	global env
	env = os.environ.copy()
	env['LC_ALL'] = 'C'
	env['COLUMNS'] = '250'
	env['HOME'] = tempfile.mkdtemp(prefix='univention-support-info_')
	try:
		shutil.copy('/etc/skel/.bashrc', env['HOME'])
		shutil.copy('/etc/skel/.profile', env['HOME'])
		f = open(os.path.join(env['HOME'], '.toprc'), 'w')
		f.write(toprc)
		f.close()
	except (OSError, IOError, ):
		pass

def cleanup():
	shutil.rmtree(env['HOME'])

def current_package_version():
	CommandTuple = ['dpkg-query', '-f', '${Version}', '-W', 'univention-support-info']
	process = subprocess.Popen(CommandTuple, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True, env=env)
	(stdout, stderr) = process.communicate()
	if process.returncode:
		log.error("current package version could not be determined: %s" % stdout.strip())
		return ''
	else:
		return stdout.strip()

def autoupdate():
	old_package_version = current_package_version()
	version_version = ucr.get('version/version')

	repository_component_ucrv = 'repository/online/component/%s' % REPOSITORY_COMPONENT
	if ucr.get(repository_component_ucrv) != 'enabled':
		config_registry.handler_set([
		'%s=enabled' % repository_component_ucrv,
		'%s/version=%s' % (repository_component_ucrv, version_version),
		'%s/description=Univention Support Tools' % repository_component_ucrv,
		])

	internet_connection = True
	CommandTuple = ['wget', '-q', '-O', os.devnull, ONLINE_REPOSITORY_URL]
	process = subprocess.Popen(CommandTuple, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True, env=env)
	(stdout, stderr) = process.communicate()
	if process.returncode:
		log.warning("No Internet connection to %s" % ONLINE_REPOSITORY_URL)
		internet_connection = False

	CommandTuple = ['grep', '-q', 'component not found: %s' % REPOSITORY_COMPONENT, '/etc/apt/sources.list.d/20_ucs-online-component.list']
	process = subprocess.Popen(CommandTuple, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True, env=env)
	(stdout, stderr) = process.communicate()
	if not process.returncode:
		log.warning("%s not reachable on repository/online/server" % repository_component_ucrv)

	univention_install_failed = False
	CommandTuple = ['univention-install', 'univention-support-info']
	process = subprocess.Popen(CommandTuple, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True, env=env)
	(stdout, stderr) = process.communicate()
	if process.returncode:
		log.warning("package autoupdate failed")
		log.debug(stdout)
		univention_install_failed = True

	new_package_version = current_package_version()

	updated = False
	# TODO: check if we updated
	CommandTuple = ['dpkg', '--compare-versions', new_package_version, 'gt', old_package_version]
	process = subprocess.Popen(CommandTuple, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True, env=env)
	(stdout, stderr) = process.communicate()
	if not process.returncode:
		log.info("Starting new script version")
	 	os.execv(sys.argv[0], sys.argv)

	if not internet_connection or univention_install_failed:
		# check if we did not update for some time
		CommandTuple = ['stat', '-c', '%Z', sys.argv[0]]
		process = subprocess.Popen(CommandTuple, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True, env=env)
		(stdout, stderr) = process.communicate()
		if process.returncode:
			log.error("stat call for %s failed: %s" % ( sys.argv[0], stdout.strip() ) )
			timestamp = None
		else:
			timestamp = stdout.strip()

		if timestamp and int(time.time()) - long(timestamp) > AGE_WARN_DAYS*24*60*60:
			url = "%s%s/maintained/component/%s/all" % ( ONLINE_REPOSITORY_URL, version_version, REPOSITORY_COMPONENT )
			log.warning("univention-support-info has not been updated for %d days, please check %s" % ( warn_days, url ) )

			answer = raw_input("Continue anyway? [Y/n]: ")
			if answer.lower() in ('n', 'no'):
				log.info("Ok, stopping as requested.\n")
				sys.exit(2)
		

def main(encrypt=False):
	checkForRoot()
	global archive, env, sambaDomainVersion
	prepareEnvironment()
	autoupdate()
	prepareArchive()

	# Check Samba Version
	if executeCommand( 'samba4-pdc-dn', (_ldapsearchCommand(), '-xLLL', '(&(univentionService=Samba 4)(objectClass=univentionDomainController))', 'dn') ):
		sambaDomainVersion = 4
	else:
		sambaDomainVersion = 3

	# Place new calls below this line
	checkMaintenance()
	collectCommandData()
	simpleFiles()
	licenseObject()
	templateFiles()
	aptPackageList()
	atJobs()
	certificateValidities()
	univentionSystemInfo()
	rotatedLogs()
	# Place new calls above this line

	closeArchive()
	print 'Data collection completed.'
	print
	print
	if encrypt:
		gpgArchiveFileName = gpg(archiveFileName)
		if gpgArchiveFileName:
			print 'The encrypted data can be found here:'
			print '    ' + gpgArchiveFileName
			print ' '
		else:
			print 'WARNING: The data could not be encrypted!'
		print
		print 'The unencrypted data can be found here:'
	else:
		print 'The data can be found here:'
	print '    ' + archiveFileName
	cleanup()

if __name__ == "__main__":
	parser = optparse.OptionParser()
	parser.add_option('--encrypt', action='store_true', dest='encrypt', default=False, help='encrypt data (can only be decrypted by Univention support)')
	parser.usage = '\n'.join(('%prog [options]',
							  'collect system information',
							  '',
							  "%prog collects information about the system's configuration.",
							  'The information is stored in a temporary tar archive, the path of which is printed to stdout.',
							  ))
	(options, args, ) = parser.parse_args()
	if args:
		parser.print_help()
		sys.exit(0)

	global log
	logging.basicConfig(filename=LOGFILE_NAME, format='%(asctime)s %(levelname)s: %(message)s', level=logging.DEBUG)
	log = logging.getLogger()
	console = logging.StreamHandler(sys.stdout)
	console.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
	console.setLevel(logging.INFO)
	log.addHandler(console)

	main(options.encrypt)
