#!/usr/bin/python2.7
# -*- coding: utf-8 -*-
#
# Univention Updater
#  A tool for installing UCS release updates
#
# Copyright (C) 2010-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
import time
import optparse
import subprocess
import traceback

import univention.config_registry

from univention.updater import UniventionUpdater, UCS_Version, ConfigurationError
import univention.updater.tools

FN_STATUS = '/var/lib/univention-updater/univention-upgrade.status'

UCR_UPDATE_AVAILABLE = 'update/available'
LOGFN = '/var/log/univention/updater.log'

configRegistry = None
logfd = None
def dprint(silent, msg, newline=True, debug=False):
	"""Print debug output."""
	if silent:
		return
	if logfd:
		if newline:
			print >>logfd, msg
		else:
			print >>logfd, '%-55s' % msg,
		logfd.flush()
	if not debug:
		if newline:
			print msg
		else:
			print '%-55s' % msg,
		sys.stdout.flush()

updater_status = {}


def update_status(**kwargs):
	'''
	update updater_status and write status to disk

	Keys:
	- current_version ==> UCS_Version ==> 2.3-1
	- next_version    ==> UCS_Version ==> 2.3-2
	- updatetype      ==> (LOCAL|NET|CDROM)
	- status          ==> (RUNNING|FAILED|DONE)
	- errorsource     ==> (SETTINGS|PREPARATION|PREUP|UPDATE|POSTUP)
	'''
	global updater_status
	updater_status.update(kwargs)
	# write temporary file
	fn = '%s.new' % FN_STATUS
	try:
		fd = open( fn, 'w+' )
		for key, val in updater_status.items():
			fd.write('%s=%s\n' % (key, val))
		fd.close()
	except:
		dprint(silent, 'Warning: cannot update %s' % fn)
	try:
		os.rename(fn, FN_STATUS)
	except:
		dprint(silent, 'Warning: cannot update %s' % FN_STATUS)


def readcontinue(msg):
	"""Print message and read yes/no/abort answer."""
	while True:
		try:
			print '%s' % msg,
			choice = raw_input().lower().strip()
			if choice  == '' or choice == 'y' or choice == 'j':
				print ''
				return True
			elif choice == 'n':
				print ''
				return False
			else:
				continue
		except KeyboardInterrupt:
			print '\n'
			return False


def _package_list(new_packages):
	"""Return comma separated list of packages."""
	l = []
	for p in new_packages:
		l.append(p[0])
	return ','.join(l)


def performUpdate(updateto=None, checkForUpdates=False, ignoressh=False, ignoreterm=False, interactive=True, silent=False):
	updater = UniventionUpdater()
	releaseUpdate = True

	if updateto:
		version_updateto = UCS_Version(updateto)
	else:
		version_updateto = None

	if not checkForUpdates:
		vv = updater.configRegistry['version/version']
		vp = updater.configRegistry['version/patchlevel']
		lastversion = '%s-%s' % (vv,vp)
		update_status( current_version=lastversion, status='RUNNING' )

	run = True

	while run and releaseUpdate:
		#######################################################################
		# RELEASE UPDATE
		#######################################################################

		# reinit updater object
		updater.ucr_reinit()

		# get next release update version
		dprint(silent, 'Checking for release updates: ', newline=False)
		next_release_update = updater.release_update_available()
		if next_release_update:
			version_next = UCS_Version(next_release_update)
			# save "first" release version in this run
			version_first = UCS_Version(next_release_update)
		else:
			version_next = None
			run = False
			dprint(silent, 'none')

		# continue update as long there's a next version and major/minor version are equal ==> do ONE major/minor update and ALL patchlevel updates
		while run and version_first.major == version_next.major and version_first.minor == version_next.minor:
			# stop release updates
			# - if no next version is available
			# - if updateto is set and updateto is smaller than next_version
			if not version_next:
				run = False
				dprint(silent, 'none')
			elif version_updateto and version_updateto < version_next:
				run = False
				dprint(silent, '%s is available but updater has been instructed to stop at version %s.' % (version_next,updateto))
			else:
				# updates available ==> stop here in "check-mode"
				if checkForUpdates:
					dprint(silent, 'found: UCS %s' % version_next)
					return True

				if interactive:
					dprint(silent, 'found: UCS %s\n' % version_next)
					run = readcontinue('Do you want to update to %s [Y|n]?' % version_next)
				else:
					dprint(silent, 'found: UCS %s\n' % version_next)

				if run:
					update_status( next_version=version_next )

					# perform release update (only one step!)
					dprint(silent, 'Starting update to UCS version %s at %s...' % (version_next, time.ctime()), debug=True)
					dprint(silent, 'Starting update to UCS version %s' % (version_next))
					time.sleep(1)
					params = ['--silent']
					if ignoressh:
						params.append('--ignoressh')
					if ignoreterm:
						params.append('--ignoreterm')
					retcode = subprocess.call(['/usr/share/univention-updater/univention-updater',  'net', '--updateto' , '%s' % (version_next)] + params , shell=False, env=os.environ)
					if retcode:
						dprint(silent, 'exitcode of univention-updater: %s' % retcode, debug=True)
						dprint(silent, 'ERROR: update failed. Please check /var/log/univention/updater.log\n')
						update_status( status='FAILED', errorsource='UPDATE' )
						sys.exit(1)
					dprint(silent, 'Update to UCS version %s finished at %s...' % (version_next, time.ctime()), debug=True)
					run_release_upgrade = True
					time.sleep(1)
				else:
					# The user said: No
					# Don't ask for a second time
					releaseUpdate = False

			if run and releaseUpdate:
				# reinit updater object
				updater.ucr_reinit()
				dprint(silent, 'Checking for release updates: ', newline=False)
				# get next release update version
				next_release_update = updater.release_update_available()
				if next_release_update:
					version_next = UCS_Version(next_release_update)
					if  version_first.major != version_next.major or version_first.minor != version_next.minor:
						dprint(silent, 'found: UCS %s (skip in first iteration)' % version_next)
				else:
					dprint(silent, 'none')
					version_next = None
					run = False

		#######################################################################
		# COMPONENT UPDATE
		#######################################################################
		# reinit updater module
		updater.ucr_reinit()
		# check if component updates are available
		dprint(silent, 'Checking for package updates: ', newline=False)
		new_packages, upgraded_packages, removed_packages = updater.component_update_get_packages()
		update_available = bool(new_packages + upgraded_packages + removed_packages)

		if update_available:
			run = True
			# updates available ==> stop here in "check-mode"
			if checkForUpdates:
				dprint(silent, 'found')
				return True

			dprint(silent, 'found\n')
			if len(removed_packages) > 0:
				dprint(silent, 'The following packages will be REMOVED:\n %s' % _package_list(removed_packages))
			if len(new_packages) > 0:
				dprint(silent, 'The following packages will be installed:\n %s' % _package_list(new_packages))
			if len(upgraded_packages) > 0:
				dprint(silent, 'The following packages will be upgraded:\n %s' % _package_list(upgraded_packages))
			if interactive:
				run = readcontinue('\nDo you want to continue [Y|n]?')

			if run:
				time.sleep(1)
				dprint(silent, 'Starting dist-update at %s...' % (time.ctime()), debug=True)
				dprint(silent, 'Starting package upgrade', newline=False )

				returncode, stdout, stderr = updater.run_dist_upgrade()

				if returncode:
					dprint(silent, 'exitcode of apt-get dist-upgrade: %s' % returncode, debug=True)
					dprint(silent, 'ERROR: update failed. Please check /var/log/univention/updater.log\n')
					update_status( status='FAILED', errorsource='UPDATE' )
					sys.exit(1)
				dprint(silent, 'dist-update finished at %s...' % (time.ctime()), debug=True)
				dprint(silent, 'done')
				time.sleep(1)
		else:
			dprint(silent, 'none')

	# updates available ==> stop here in "check-mode"
	if checkForUpdates:
		return False


def main():
	global configRegistry
	global logfd

	parser = optparse.OptionParser( )
	parser.add_option("--updateto", dest="updateto", default=None, action="store", help="update up to specified version")
	parser.add_option("--check", dest="check", default=False, action="store_true", help="check if updates are available")
	parser.add_option("--setucr", dest="setucr", default=False, action="store_true", help="if set, variable update/available will be updated if --check is specified too")
	parser.add_option("--ignoressh", dest="ignoressh", default=False, action="store_true", help="pass --ignoressh to univention-updater")
	parser.add_option("--ignoreterm", dest="ignoreterm", default=False, action="store_true", help="pass --ignoreterm to univention-updater")
	parser.add_option("--noninteractive", dest="noninteractive", default=False, action="store_true", help="Perform a non-interactive update")
	parser.add_option("--iso", dest="iso", default=None, action="store", help="ISO image for the repository update")
	parser.add_option("--cdrom", dest="cdrom", default="/dev/cdrom", action="store", help="CDROM device for the repository update")
	(options, args) = parser.parse_args()

	try:
		logfd=open(LOGFN, 'a+')
	except:
		print 'Cannot open %s for writing' % LOGFN
		sys.exit(1)

	configRegistry = univention.config_registry.ConfigRegistry()
	configRegistry.load()

	if options.noninteractive:
		os.environ['UCS_FRONTEND']='noninteractive'

	silent = False
	dprint(silent, '\nStarting univention-upgrade. Current UCS version is %(version/version)s-%(version/patchlevel)s errata%(version/erratalevel)s\n' % configRegistry)

	dprint(silent, 'Checking for local repository: ', newline=False)
	if options.check:
		dprint(silent, 'skipped')
	elif configRegistry.is_true('local/repository', False) and configRegistry.is_true('repository/mirror', False):
		dprint(silent, 'found\n')
		if options.noninteractive or readcontinue('Update the local repository via network [Y|n]?'):
			if options.updateto:
				subprocess.call(('/usr/sbin/univention-repository-update', 'net', '--updateto', options.updateto))
			else:
				subprocess.call(('/usr/sbin/univention-repository-update', 'net'))
		elif options.noninteractive or readcontinue('Update the local repository via cdrom [Y|n]?'):
			if options.iso:
				subprocess.call(('/usr/sbin/univention-repository-update', 'cdrom', '--iso', options.iso))
			else:
				subprocess.call(('/usr/sbin/univention-repository-update', 'cdrom', '--device', options.cdrom))
	else:
		dprint(silent, 'none')

	if options.check:
		try:
			update_available = performUpdate(options.updateto, checkForUpdates=True, ignoressh=options.ignoressh, ignoreterm=options.ignoreterm, interactive=False, silent=False)
		except SystemExit, e:
			sys.exit(e)
		except ConfigurationError, e:
			print >>sys.stderr, 'The connection to the repository server failed: %s. Please check the repository configuration and the network connection.' % e
			sys.exit(3)
		except Exception, e:
			print 'An error occurred - stopping here.'
			import traceback
			print >>logfd, 'Traceback in univention-upgrade:'
			print >>logfd, traceback.format_exc()
			sys.exit(2)
		if update_available:
			dprint(silent, 'Please rerun command without --check argument to install.')
			if options.setucr:
				univention.config_registry.handler_set( [ '%s=yes' % UCR_UPDATE_AVAILABLE ] )
			sys.exit(0)
		else:
			print 'No update available.'
			if options.setucr:
				univention.config_registry.handler_set( [ '%s=no' % UCR_UPDATE_AVAILABLE] )
			sys.exit(1)
	else:
		try:
			performUpdate(options.updateto, ignoressh=options.ignoressh, ignoreterm=options.ignoreterm, interactive=not options.noninteractive, silent=False)
		except SystemExit, e:
			sys.exit(e)
		except ConfigurationError, e:
			update_status( status='FAILED', errorsource='SETTINGS' )
			print >>sys.stderr, 'The connection to the repository server failed: %s. Please check the repository configuration and the network connection.' % e
			sys.exit(3)
		except Exception, e:
			update_status( status='FAILED' )
			print 'An error occurred - stopping here.'
			import traceback
			print >>logfd, 'Traceback in univention-upgrade:'
			print >>logfd, traceback.format_exc()
			sys.exit(2)
		update_status( status='DONE' )

		# check for new updates after updating ; update UCR variable
		#
		# BUG: After an release upgrade this process MUST NOT continue to use old python, ucr, updater, ...
		try:
			update_available = performUpdate(options.updateto, checkForUpdates=True, ignoressh=options.ignoressh, ignoreterm=options.ignoreterm, interactive=False, silent=True)
		except SystemExit, e:
			sys.exit(e)
		except ConfigurationError, e:
			print >>sys.stderr, 'The connection to the repository server failed: %s. Please check the repository configuration and the network connection.' % e
			sys.exit(3)
		except Exception, e:
			print 'An error occurred - stopping here.'
			import traceback
			print >>logfd, 'Traceback in univention-upgrade:'
			print >>logfd, traceback.format_exc()
			sys.exit(2)
		configRegistry.load()
		if update_available and configRegistry.is_false(UCR_UPDATE_AVAILABLE, True):
			univention.config_registry.handler_set( [ '%s=yes' % UCR_UPDATE_AVAILABLE ] )
		elif not update_available and configRegistry.is_true(UCR_UPDATE_AVAILABLE, True):
			univention.config_registry.handler_set( [ '%s=no' % UCR_UPDATE_AVAILABLE] )


if __name__ == '__main__':
	try:
		lock = univention.updater.tools.updater_lock_acquire()
	except univention.updater.tools.LockingError, e:
		print >>sys.stderr, e
		sys.exit(5)
	try:
		main()
	finally:
		if not univention.updater.tools.updater_lock_release(lock):
			print 'WARNING: updater-lock already released!'
