#!/usr/bin/python2.7
# -*- coding: utf-8 -*-
#
# Univention Updater
#  creates a repository
#
# 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/>.
#

import os
import errno
from optparse import OptionParser
import shutil
import subprocess
import sys
from textwrap import dedent

import univention.config_registry as ucr
import univention.updater.repository as urepo
import univention.updater.tools
from univention.updater.tools import UCS_Version

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

_mirror_base = configRegistry.get( 'repository/mirror/basepath', '/var/lib/univention-repository' )
_repo_base = "" # this path is set later in __main__

def check_preconditions( options ):
	""" Check for already existing mirror and for debmirror package """
	# check directories
	if os.path.exists( os.path.join( _mirror_base, 'mirror' ) ):
		print >> sys.stderr, 'Warning: The path %s/mirror already exists.' % _mirror_base

	if options.interactive:
		print "Are you sure you want to create a local repository? [yN] ",
		sys.stdin.flush()
		if not sys.stdin.readline().startswith( 'y' ):
			print >> sys.stderr, 'Aborted.'
			sys.exit( 1 )

	# install univention-debmirror
	print 'Installing univention-debmirror'
	ret=subprocess.call( ['univention-install', '--yes', 'univention-debmirror' ] )
	if ret != 0:
		print >> sys.stderr, 'Error: Failed to install univention-debmirror'
		sys.exit( 1 )

	ret, msg = urepo.is_debmirror_installed()
	if not ret:
		print >> sys.stderr, msg
		sys.exit( 1 )

def  prepare( options ):
	""" Set local/repository and create directory structure """
	# set local/repository
	if configRegistry.is_false( 'local/repository', True ):
		ucr.handler_set( [ 'local/repository=yes' ] )
		configRegistry.load()

	if configRegistry.is_false( 'repository/mirror', True):
		ucr.handler_set( [ 'repository/mirror=yes' ] )
		configRegistry.load()

	# create directory structure
	try:
		os.makedirs( _repo_base )
	except OSError, ex:
		# already exists -> ignore
		if ex.errno != errno.EEXIST:
			raise ex
	for arch in urepo.ARCHITECTURES:
		try:
			os.makedirs( os.path.join( _repo_base, arch ) )
		except OSError, ex:
			# already exists -> ignore
			if ex.errno != errno.EEXIST:
				raise ex

def copy_repository( options ):
	""" Copy version info, kernels, grub configuration, profiles, packages and dists """
	print 'Copying data. Please be patient ...'

	print '  copying version information ...',
	try:
		shutil.copy2( os.path.join( options.mount_point, '.univention_install' ), _mirror_base )
	except:
		print 'failed.'
	else:
		print 'done.'

	# copy kernel and grub config
	if options.major_version < 4:
		print '  copying kernel and boot configuration ...',
		boot_dest = os.path.join( _mirror_base, 'boot' )
		if os.path.isdir( boot_dest ):
			shutil.rmtree( boot_dest )
		try:
			shutil.copytree( os.path.join( options.mount_point, 'boot' ), boot_dest )
		except shutil.Error, ex:
			print "failed (%s)." % (ex,)
		else:
			print 'done.'

		# copy profiles
		if os.path.exists(os.path.join(options.mount_point, 'profiles')):
			print '  copying profiles ...',
			profiles_dest = os.path.join( _mirror_base, 'profiles' )
			if os.path.isdir( profiles_dest ):
				shutil.rmtree( profiles_dest )
			try:
				shutil.copytree(os.path.join(options.mount_point, 'profiles'), profiles_dest)
			except shutil.Error, ex:
				print "failed (%s)." % (ex,)
			else:
				# everyone should be able to read the profiles
				os.chmod( profiles_dest, 0555 )
				print 'done.'

	# copy packages to new directory structure
	print '  copying packages ...',
	sys.stdout.flush()
	for subdir in ('packages', 'all', 'amd64', 'i386'):
		if os.path.exists(os.path.join(options.mount_point, subdir)):
			urepo.copy_package_files(os.path.join(options.mount_point, subdir), _repo_base)
			sys.stdout.flush()
	print "done."

	# copy dists directory structure
	print '  copying dists ...',
	dists_dest = os.path.join( _repo_base, 'dists' )
	if os.path.isdir( dists_dest ):
		shutil.rmtree( dists_dest )
	try:
		# handle the different dists directories (UCS installer or d-i)
		for dists_src in (os.path.join(options.mount_point, 'packages', 'dists'), os.path.join(options.mount_point, 'dists')):
			if os.path.isdir(dists_src):
				shutil.copytree(dists_src, dists_dest, symlinks=True)
	except shutil.Error, ex:
		print "failed (%s)." % (ex,)
	else:
		print 'done.'

def mount( options ):
	""" Mount CDROM and check for valid medium """
	if options.interactive:
		# ask user to insert cdrom
		print '\nPlease insert a UCS installation medium and press <Enter>',
		sys.stdin.readline()
	if options.mount:
		print "Mounting %s ..." % options.mount_point,
		if options.iso:
			cmd = ('mount', '-o', 'loop,ro', options.iso, options.mount_point)
		else:
			cmd = ('mount', '-o', 'ro', options.mount_point)
		devnull = open( os.path.devnull, 'w' )
		try:
			ret = subprocess.call(cmd, stdout=devnull, stderr=subprocess.STDOUT)
		finally:
			devnull.close()
		# if exit code is 0 or 32 (already mounted)
		if ret in (0, 32):
			print 'done.'
		else:
			print 'failed.'
			return False

	print "Checking medium in %s ..." % options.mount_point,
	found_error = True
	if os.path.isdir(os.path.join(options.mount_point, 'packages')) and os.path.isdir(os.path.join(options.mount_point, 'profiles')):
		found_error = False
	elif os.path.exists(os.path.join(options.mount_point, '.univention_install')) and \
			os.path.isdir(os.path.join(options.mount_point, 'all')) and \
			(os.path.isdir(os.path.join(options.mount_point, 'amd64')) or \
				 os.path.isdir(os.path.join(options.mount_point, 'i386'))):
		found_error = False
	if found_error:
		print 'failed.'
		print >> sys.stderr, 'Error: This is not an UCS installation medium.'
		return False

	print 'ok.'
	return True

def setup_repository( options ):
	""" Update indexes """
	urepo.update_indexes( _repo_base, dists = True )

	basepath = configRegistry.get('repository/mirror/basepath', '/var/lib/univention-repository')
	for p in ['var', 'skel']:
		d = os.path.join(basepath, p)
		if not os.path.exists(d):
			os.mkdir(d)

def setup_pxe( options ):
	'''setup network installation (PXE)'''
	pxedir = '/var/lib/univention-client-boot'
	installerdir = os.path.join(pxedir, 'installer')

	if not os.path.exists(pxedir):
		os.makedirs(pxedir)

	if options.major_version >= 4:
		installerdir = os.path.join(pxedir, 'installer', '%d.%d-%d' % (options.major_version, options.minor_version, options.patchlevel_version))
		if not os.path.exists(installerdir):
			os.makedirs(installerdir)

		# copy kernel and initrd to /var/lib/univention-client-boot/installer/<major>.<minor>-<patchlevel>/
		# and create/refresh symlinks in /var/lib/univention-client-boot/ to these files
		for fn in ['linux', 'initrd.gz']:
			srcfn = os.path.join(options.mount_point, 'netboot', fn)
			dstfn = os.path.join(installerdir, fn)
			symlinkfn = os.path.join(pxedir, fn)
			if os.path.exists(srcfn):
				shutil.copy2(srcfn, dstfn)
				if os.path.islink(symlinkfn):
					os.remove(symlinkfn)
				os.symlink(os.path.relpath(dstfn, pxedir), symlinkfn)
	else:
		print 'WARNING: The usage of this DVD for PXE reinstallation is not possible.'
		print '         Please use an UCS installation DVD with UCS 4.0-0 or later.'

if __name__ == '__main__':
	for mount_point_default in ('/cdrom', '/media/cdrom', '/media/cdrom0'):
		if os.path.isdir(mount_point_default):
			break
	parser = OptionParser( usage = "usage: %prog [options]" )
	parser.add_option( '-n', '--non-interactive', action = 'store_false',
					   dest = 'interactive', default = True,
					   help = 'if given no questions are asked.' )
	parser.add_option( '-N', '--no-mount', action = 'store_false',
					   dest = 'mount', default = True,
					   help = 'mounting the installation media is not required' )
	parser.add_option( '-s', '--silent', action = 'store_true',
					   dest = 'silent', default = False,
					   help = 'do not print any information, just errors and warnings' )
	parser.add_option( '-m', '--mount-point', action = 'store',
					   dest = 'mount_point', default = mount_point_default,
					   help = 'devices mount point for CD-ROM drive' )
	parser.add_option('-i', '--iso', action='store', dest='iso',
	                  default=None, help='define filename of an ISO image')

	( options, arguments ) = parser.parse_args()

	if options.silent:
		sys.stdout = open(os.path.devnull, 'w')

	try:
		lock = univention.updater.tools.updater_lock_acquire()
	except univention.updater.tools.LockingError, ex:
		print >>sys.stderr, ex
		sys.exit(5)
	try:
		check_preconditions( options )

		if not mount( options ):
			print >>sys.stderr, "Error: Failed to mount CD-ROM device at %s" % options.mount_point
			sys.exit( 1 )

		#define repository base path with information from image
		installfile = os.path.join(options.mount_point, '.univention_install')
		for line in open(installfile).readlines():
			if line.startswith('VERSION='):
				version = line.split('=', 1)[1]
				options.major_version = int(version.split('.', 1)[0].strip())
				options.minor_version = int(version.split('.', 1)[1].strip())
			elif line.startswith('PATCHLEVEL='):
				options.patchlevel_version = int(line.split('=', 1)[1].strip())

		_repo_base = os.path.join(_mirror_base,
								  'mirror',
								  '%s.%s' % (options.major_version, options.minor_version),
								  'maintained',
								  '%s.%s-0' % (options.major_version, options.minor_version))

		prepare( options )

		try:
			copy_repository( options )
			setup_repository( options )
			setup_pxe( options )
		finally:
			if options.mount:
				subprocess.call( [ 'umount', options.mount_point ] )

		# set repository server to local system
		fqdn = '%s.%s' % ( configRegistry.get( 'hostname' ), configRegistry.get( 'domainname' ) )
		ucr.handler_set( [ 'repository/online/server=%s' % fqdn ] )

		# unset UCR variable marking old repository
		if 'repository/local/old' in configRegistry:
			ucr.handler_unset(['repository/local/old'])

		# set start version for synchronsation of repository
		if options.major_version:
			ucr.handler_set( [ 'repository/mirror/version/start?%s.0-0' % options.major_version ] )

		dvd_version = UCS_Version((options.major_version, options.minor_version, options.patchlevel_version))
		if not configRegistry.get('repository/mirror/version/end', '').strip() or (UCS_Version(configRegistry.get('repository/mirror/version/end').strip()) < dvd_version):
			ucr.handler_set(['repository/mirror/version/end=%d.%d-%d' % (options.major_version,
																		 options.minor_version,
																		 options.patchlevel_version)])

		# create symbolic link univention-repository
		try:
			basepath = configRegistry.get('repository/mirror/basepath', '/var/lib/univention-repository')
			os.symlink('.', os.path.join(basepath, 'mirror', 'univention-repository'))
		except OSError, ex:
			if ex.errno != errno.EEXIST:
				raise

		print dedent(r"""
		The local repository has been created.

		The local host has been modified to use this local repository.  Other hosts
		must be re-configured by setting the Univention Configuration Registry (UCR)
		variable 'repository/online/server' to the FQDN of this host.

		  ucr set repository/online/server="%(hostname)s.%(domainname)s"

		UCS validates the archive integrity through signed Release files (using the
		secure APT mechanism).  Secure APT is not yet available for local repositories.
		As such, it must be disabled on this and all other hosts using this
		repository by setting the UCR variable 'update/secure_apt' to no:

		  ucr set update/secure_apt=no

		Both settings are best set in a domain by defining UCR Policies, which
		set these variables on all hosts using this repository server. For example:

		  udm policies/repositoryserver create \
		    --position "cn=repository,cn=update,cn=policies,%(ldap/base)s" \
		    --set name="%(hostname)s repository" \
		    --set repositoryServer="%(hostname)s.%(domainname)s"
		  udm policies/registry create \
		    --position "cn=config-registry,cn=policies,%(ldap/base)s" \
		    --set name="global settings" \
		    --set registry="update/secure_apt no"
		  udm container/dc modify \
		    --dn "%(ldap/base)s" \
		    --policy-reference "cn=global settings,cn=config-registry,cn=policies,%(ldap/base)s" \
		    --policy-reference "cn=%(hostname)s repository,cn=repository,cn=update,cn=policies,%(ldap/base)s"
		""" % configRegistry)

		if not options.minor_version == 0:
			print dedent("""
			An UCS repository must always start with minor version 0, for example
			with UCS %(major)s.0. Please synchronize the repository from %(major)s.0 to %(major)s.%(minor)s
			by using the tool univention-repository-update.
			""" % {'major': options.major_version, 'minor': options.minor_version})

	finally:
		if not univention.updater.tools.updater_lock_release(lock):
			print 'WARNING: updater-lock already released!'
