#!/usr/bin/python2.7
# -*- coding: utf-8 -*-
#
# Univention Updater
#  actualise script
#
# Copyright 2004-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/>.

# TODO: use UCR variables for update, upgrade, install and remove commands

import os, sys, time, tempfile, commands
import shlex
import subprocess

import univention.config_registry as ucr
from univention.updater.commands import *
import univention.updater.tools

LOGNAME = '/var/log/univention/actualise.log'

configRegistry = ucr.ConfigRegistry()
configRegistry.load()

ldap_hostdn = configRegistry.get('ldap/hostdn')

class Tee( object ):
	'''Writes the given string to serveral files at once. Could by used
	with the print statement'''
	def __init__( self, files = [], stdout = True, filter = None ):
		self.stdout = stdout
		self.files = files
		self.filter = filter

	def call( self, command, **kwargs ):
		devnull = None
		p = subprocess.Popen( command, stdout = subprocess.PIPE, stderr = subprocess.STDOUT, **kwargs )
		tee_command = [ 'tee', '-a' ] + self.files
		if self.stdout:
			if self.filter:
				tee = subprocess.Popen( tee_command, stdin = p.stdout, stdout = subprocess.PIPE, stderr = subprocess.STDOUT )
				egrep = subprocess.Popen( [ 'egrep', self.filter ], stdin = tee.stdout )
				ret = egrep.wait()
			else:
				tee = subprocess.Popen( tee_command, stdin = p.stdout )
		else:
			devnull = open( os.path.devnull, 'a' )
			tee = subprocess.Popen( tee_command, stdin = p.stdout, stdout = devnull )
			devnull.close()

		# Must wait for exit from back to front, only the exit status of p is relevant
		ret = tee.wait()
		ret = p.wait()
		if devnull:
			devnull.close()

		return ret

def usage():
	print "univention-actualise: Perform a (dist-)upgrade and (un-)install packages as set"
	print "                      through policies"
	print ""
	print "usage: univention-actualise [-? || --help] [--dist-upgrade] [--silent] [--check]"
	print ""
	print "-?, --help       Show this message"
	print "--dist-upgrade   Perform a dist-upgrade instead of a regular update"
	print "--silent         Don't show normal output, but error messages only"
	print "--check          Don't do anything, just check if updates are available"

def waitForLock():
	# Wait for /var/run/apt-get.lock to vanish
	count=0
	while os.path.exists('/var/run/apt-get.lock') and count < 300:
		print("Waiting for /var/run/apt-get.lock to vanish...")
		time.sleep(1)
		count=count+1

def createLock():
	try:
		fd = open( '/var/run/apt-get.lock', 'w' )
		fd.close()
	except:
		pass

def getUpdate( configRegistry ):
	# Small function waiting for apt lockfile to vanish then starts apt-get update

	print "Running apt-get update"
	waitForLock()
	createLock()
	logfile = open( LOGNAME, 'a' )
	res = subprocess.call(shlex.split(cmd_update), stdout=logfile, stderr=logfile)
	logfile.close()
	if os.path.exists('/var/run/apt-get.lock'):
		os.unlink('/var/run/apt-get.lock')
	if res != 0:
		print >>sys.stderr, "E: failed to update"
		sys.exit(res)

def deactivateSourcesListMethods( methods = [ 'cdrom' ] ):
	cnt = 0
	lines = []
	deactivated_lines = []
	f = open('/etc/apt/sources.list', 'r')
	for line in f.readlines():
		line=line.strip(' \n\t')
		for method in methods:
			if line.startswith( 'deb %s:' % method ) or line.startswith( 'deb-src %s:' % method ):
				line = '#%s' % line
				deactivated_lines.append(line)
				cnt += 1
		lines.append(line)
	f.close()
	if cnt:
		f = open('/etc/apt/sources.list', 'w')
		f.write( '\n'.join( lines ) )
		f.write( '\n' )
		f.close()
		debug_file=open(LOGNAME, 'a+')
		debug_file.write('Hint: deactivated %d lines in /etc/apt/sources.list:\n' % cnt)
		debug_file.write( '   %s\n' % '\n   '.join(deactivated_lines) )
		debug_file.close()

def check( configRegistry, dist_upgrade = False):
	# Just probe if there are packages to add or remove

	actualise = False
	getUpdate( configRegistry )

	# Probe for packages to actualise
	if dist_upgrade:
		cmd = cmd_dist_upgrade_sim
	else:
		cmd = cmd_upgrade_sim

	waitForLock()
	createLock()
	res=commands.getoutput( '%s | egrep ".*[0-9] upgraded, [0-9].*"' % ( cmd ) )
	if os.path.exists('/var/run/apt-get.lock'):
		os.unlink('/var/run/apt-get.lock')
	sres=res.split()

	# These ones are nicer to read
	upgraded = int(sres[0])
	newlyinstalled = int(sres[2])
	remove = int(sres[5])
	notupgraded = int(sres[9])

	# If there are any pakages to upgrade, install or remove
	if upgraded != 0 or newlyinstalled != 0 or remove != 0:
		actualise = True

	# Probe for policies
	rem_packages=getPackageList(configRegistry, 'remove')
	add_packages=getPackageList(configRegistry, 'add')

	if rem_packages or add_packages or actualise:
		return 1
	else:
		return 0

def getPackageList(configRegistry, job):
	# Get a list of packages to remove or add, depending on the value of job
	# getPackageList(configRegistryObject, pkgdb, job={add,remove})

	packageList=[]
	packages_name=""

	if job == 'remove':
		if configRegistry['server/role'] == 'fatclient':
			packages_name='univentionClientPackagesRemove='
		elif configRegistry['server/role'] == 'mobileclient':
			packages_name='univentionMobileClientPackagesRemove='
		elif configRegistry['server/role'] == 'memberserver':
			packages_name='univentionMemberPackagesRemove='
		elif configRegistry['server/role'] == 'domaincontroller_slave':
			packages_name='univentionSlavePackagesRemove='
		elif configRegistry['server/role'] == 'domaincontroller_master' or configRegistry['server/role'] == 'domaincontroller_backup':
			packages_name='univentionMasterPackagesRemove='
	elif job == 'add':
		if configRegistry['server/role'] == 'fatclient':
			packages_name='univentionClientPackages='
		elif configRegistry['server/role'] == 'mobileclient':
			packages_name='univentionMobileClientPackages='
		elif configRegistry['server/role'] == 'memberserver':
			packages_name='univentionMemberPackages='
		elif configRegistry['server/role'] == 'domaincontroller_slave':
			packages_name='univentionSlavePackages='
		elif configRegistry['server/role'] == 'domaincontroller_master' or configRegistry['server/role'] == 'domaincontroller_backup':
			packages_name='univentionMasterPackages='
	else:
		print >>sys.stderr, "E: no valid job defined"
		sys.exit(1)

	p1 = subprocess.Popen(['univention_policy_result', '-D', ldap_hostdn, '-y', '/etc/machine.secret', '-s', ldap_hostdn], stdout=subprocess.PIPE)
	result = p1.communicate()[0]

	if p1.returncode != 0:
		print >>sys.stderr, 'failed to execute univention_policy_result'
		sys.exit(p1.returncode)

	for line in result.split('\n'):
		line=line.strip(' ').replace('"','')
		if line.startswith(packages_name):
			package=line.strip('\n').replace('%s' % packages_name, '').split('/')[-1]
			packageList.append(package)
	return packageList

def main():
	os.putenv('PATH', '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/bin/X11')
	os.environ['LC_ALL'] = 'C'

	pkgdb = None
	pkgdb_scope = None
	silent = False
	print_usage = False
	dist_upgrade = False
	check_only = False

	# parse arguments
	for arg in sys.argv[1:]:
		if arg == "--silent":
			silent = True
		elif arg == "-?" or arg=="--help":
			print_usage = True
		elif arg == "--dist-upgrade":
			dist_upgrade = True
		elif arg == '--check':
			check_only = True
		else:
			# unknown parameter
			print_usage=True

	if print_usage:
		usage()
		sys.exit(0)

	if silent:
		# redirect stdout to /dev/null
		sys.stdout = open("/dev/null", "w")

	try:
		if check_only:
			# Only probe for packages to add/remove
			return check(configRegistry, dist_upgrade)

		if ldap_hostdn:
			logfile = open( LOGNAME, 'a' )
			logfile.write('***** Starting univention-actualise at %s\n' % time.ctime())

			deactivateSourcesListMethods( methods = [ 'cdrom' ] )

			getUpdate( configRegistry )

			# temporarily disable pkgdb
			pkgdb_scan = configRegistry.get('pkgdb/scan', getscope=True)
			if pkgdb_scan:
				# get value and UCR scope of variable pkgdb/scan
				pkgdb_scope, pkgdb = pkgdb_scan
				if pkgdb:
					# disable pkgdb in UCR scope FORCED
					ucr.handler_set(['pkgdb/scan=no'], {'force':True})

			rem_packages=getPackageList(configRegistry, 'remove')
			for package in rem_packages:
				waitForLock()

				# check if the package exists
				res = subprocess.call(shlex.split(cmd_show) + [package], stdout=logfile, stderr=logfile)
				if res == 0:
					print "Removing packages: %s" % package
					createLock()
					os.environ[ 'DEBIAN_FRONTEND' ] = 'noninteractive'
					res = subprocess.call(shlex.split(cmd_config), stdout=logfile, stderr=logfile)
					if not res:
						res = subprocess.call(shlex.split(cmd_remove) + [package], stdout=logfile, stderr=logfile)
				else:
					print("The package %s doesn't exist." % package)
					res=0
				if os.path.exists('/var/run/apt-get.lock'):
					os.unlink('/var/run/apt-get.lock')
				if res != 0:
					print >>sys.stderr, "E: failed to remove %s" % package
					sys.exit(res)


			add_packages=getPackageList(configRegistry, 'add')
			for package in add_packages:
				waitForLock()
				res = subprocess.call(shlex.split(cmd_show) + [package], stdout=logfile, stderr=logfile)
				if res == 0:
					print "Installing packages: %s" % package
					createLock()
					os.environ[ 'DEBIAN_FRONTEND' ] = 'noninteractive'
					res = subprocess.call(shlex.split(cmd_config), stdout=logfile, stderr=logfile)
					if not res:
						res = subprocess.call(shlex.split(cmd_install) + [package], stdout=logfile, stderr=logfile)
				else:
					print("The package %s doesn't exist." % package)
					res=0

				if os.path.exists('/var/run/apt-get.lock'):
					os.unlink('/var/run/apt-get.lock')
				if res != 0:
					print >>sys.stderr, "E: failed to install %s" % package
					sys.exit(res)

			waitForLock()

		else:
			# ldap/hostdn is not set
			if configRegistry['server/role'] != 'basesystem':
				print >>sys.stderr, "W: ldap/hostdn is not set - please run univention-join"

		if dist_upgrade:
			msg = "Dist-upgrading system"
			cmd = cmd_dist_upgrade
		else:
			msg = "Upgrading system"
			cmd = cmd_upgrade

		print msg
		# TODO: use mkstemp and close directly the file descriptor

		createLock()
		os.environ[ 'DEBIAN_FRONTEND' ] = 'noninteractive'
		tee = Tee( [ LOGNAME ], stdout = not silent  )
		res = tee.call(shlex.split(cmd_config))
		if res != 0:
			print >>sys.stderr, "E: failed to configure packets, see %s for details." % LOGNAME
		else:
			tee = Tee( [ LOGNAME ], stdout = not silent, filter = '(^Get|^Unpacking|^Preparing|^Setting up|packages upgraded)' )
			res = tee.call( cmd.split( ' ' ) )
			if res != 0:
				print >>sys.stderr, "E: failed to upgrade, see %s for details." % LOGNAME

		if os.path.exists('/var/run/apt-get.lock'):
			os.unlink('/var/run/apt-get.lock')

		sys.exit(res)

	finally:
		if pkgdb:
			if pkgdb_scope == ucr.ConfigRegistry.FORCED:
				# old value was set in FORCED scope
				ucr.handler_set(['pkgdb/scan=%s' % pkgdb], {'force': True})
			else:
				# old value was set in any other scope ==> remove value in FORCED scope
				ucr.handler_unset(['pkgdb/scan'], {'force': True})

			if str(pkgdb).lower() in ("yes","enable","enabled","true","1"):
				if not silent:
					os.system('/usr/sbin/univention-pkgdb-scan')
				else:
					os.system('/usr/sbin/univention-pkgdb-scan > /dev/null')

def update_ucr_updatestatus():
	try:
		if configRegistry.is_true('update/umc/updateprocess/easy', False):
			devnull = open(os.path.devnull, 'w' )
			subprocess.call( '/usr/share/univention-updater/univention-updater-check', stdout = devnull, stderr = devnull )
			devnull.close()
	except:
		dprint('Warning: calling univention-updater-check failed.')

if __name__ == '__main__':
	res = 0
	do_ucr_update = False
	try:
		lock = univention.updater.tools.updater_lock_acquire()
	except univention.updater.tools.LockingError, e:
		print >>sys.stderr, e
		sys.exit(5)
	# use nested try-finally-try-except since python2.4 does not support http://www.python.org/dev/peps/pep-0341/
	try:
		try:
			failure = '/var/lib/univention-updater/update-failed'
			if os.path.exists(failure):
				print 'univention-actualise: univention-updater failed, stopping...'
				print '	   remove `%s\' to proceed' % failure
				sys.exit(2)

			try:
				for root, dirs, files in os.walk('/etc/apt/sources.list.d'):
					for file in [file for file in files if file.startswith('00_ucs_temporary_')]:
						filename = os.path.join(root, file)
						print 'Warning: Deleting `%s` from incomplete update.' % filename
						os.remove(filename)
					del dirs[:]
			except:
				print 'Failed, aborting.'
				sys.exit(2)

			res = main()
		except SystemExit, e:
			if e.args[0] == 0:
				do_ucr_update = True

	finally:
		if not univention.updater.tools.updater_lock_release(lock):
			print 'WARNING: updater-lock already released!'
		if do_ucr_update:
			update_ucr_updatestatus()
	sys.exit(res)
