#! /usr/bin/env python
# -*- coding: utf-8 -*-

# opsi-makeproductfile is part of the desktop management solution opsi
# (open pc server integration) http://www.opsi.org
# Copyright (C) 2010-2016 uib GmbH <info@uib.de>

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.

# This program is distributed 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
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
opsi-makeproductfile - create opsi-packages for deployment.

:copyright:	uib GmbH <info@uib.de>
:author: Niko Wenselowski <n.wenselowski@uib.de>
:author: Jan Schneider <j.schneider@uib.de>
:license: GNU Affero General Public License version 3
"""

import fcntl
import gettext
import os
import re
import struct
import sys
import termios
import tty

import OPSI.Util.File.Archive
from OPSI.Logger import LOG_DEBUG, LOG_ERROR, LOG_NONE, LOG_WARNING, Logger
from OPSI.System import execute
from OPSI.Types import forceFilename, forceUnicode
from OPSI.Util.Message import ProgressObserver, ProgressSubject
from OPSI.Util.Product import ProductPackageSource
from OPSI.Util.File.Opsi import PackageControlFile
from OPSI.Util.File import ZsyncFile
from OPSI.Util import md5sum

try:
	import argparse
except ImportError:
	from OPSI.Util import argparse

__version__ = '4.0.6.14'

logger = Logger()

try:
	t = gettext.translation('opsi-utils', '/usr/share/locale')
	_ = t.ugettext
except Exception as e:
	logger.error(u"Locale not found: %s" % e)

	def _(string):
		return string


class ProgressNotifier(ProgressObserver):
	def __init__(self):
		self.usedWidth = 60
		try:
			tty = os.popen('tty').readline().strip()
			fd = open(tty)
			terminalWidth = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234'))[1]
			if (self.usedWidth > terminalWidth):
				self.usedWidth = terminalWidth
			fd.close()
		except Exception:
			pass

	def progressChanged(self, subject, state, percent, timeSpend, timeLeft, speed):
		if (subject.getEnd() <= 0):
			return
		barlen = self.usedWidth - 10
		filledlen = int("%0.0f" % (barlen * percent / 100))
		bar = u'='*filledlen + u' ' * (barlen - filledlen)
		percent = '%0.2f%%' % percent
		sys.stderr.write('\r %8s [%s]\r' % (percent, bar))
		sys.stderr.flush()

	def messageChanged(self, subject, message):
		sys.stderr.write('\n%s\n' % message)
		sys.stderr.flush()


def main(argv):
	os.umask(022)

	logger.setConsoleLevel(LOG_WARNING)
	logger.setConsoleColor(True)
	logger.setConsoleFormat(u'%L: %M')

	parser = argparse.ArgumentParser(add_help=False,
		description=("Provides an opsi package from a package source directory.\n"
					"If no source directory is supplied, the current directory will be used.")
	)
	parser.add_argument('--help', action='store_true', default=False,
						help="Show help.")  # Manual implementation because of -h
	parser.add_argument('--version', '-V', action='version', version=__version__)
	parser.add_argument('--quiet', '-q', action='store_true', default=False,
						help="do not show progress")
	parser.add_argument('--verbose', '-v', default=False, action="store_true",
						help="verbose")
	parser.add_argument('--log-level', '-l', dest="logLevel", type=int,
						default=LOG_WARNING, help="Log-level 0..9")
	parser.add_argument('-n', dest="compression", help="Do not compress",
						default='gzip', action='store_const', const=None)
	parser.add_argument('-F', dest="format", default='cpio', choices=['cpio', 'tar'],
						help="archive format [tar|cpio], default: cpio")
	parser.add_argument('-h', dest="dereference", help="follow symlinks",
						default=False, action='store_true')
	parser.add_argument('-I', dest="incremental", help="incremental package",
						metavar='required version', default=0)
	customGroup = parser.add_mutually_exclusive_group()
	customGroup.add_argument('-i', dest="customName", default=u'',
							metavar='custom name',
							help="custom name (add custom files)")
	customGroup.add_argument('-c', dest="customOnly", default=False,
							metavar='custom name',
							help="custom name (custom only)")
	parser.add_argument('-C', dest="compatibilityMode", default=False,
						action="store_true", help="compatibility mode (opsi 3)")
	parser.add_argument('-t', dest="tempDir", help="temp dir", default=u'/tmp',
						metavar='directory')
	parser.add_argument('-m', dest="createMd5SumFile", help="create md5sum file",
						default=False, action='store_true')
	parser.add_argument('-z', dest="createZsyncFile", help="create zsync file",
						default=False, action='store_true')
	parser.add_argument('--no-pigz', dest="disablePigz",
						default=False, action='store_true',
						help="Disable the usage of pigz")
	parser.add_argument('packageSourceDir', metavar="source directory",
						nargs='?', default=os.getcwd())

	args = parser.parse_args()

	if args.help:
		parser.print_help()
		sys.exit(1)

	customName = args.customName
	customOnly = bool(args.customOnly)
	if customOnly:
		customName = args.customOnly
	requiredVersion = args.incremental
	dereference = args.dereference
	logLevel = args.logLevel
	compression = args.compression
	quiet = args.quiet
	tempDir = forceFilename(args.tempDir)
	format = forceUnicode(args.format)
	compatibilityMode = args.compatibilityMode
	createMd5SumFile = args.createMd5SumFile
	createZsyncFile = args.createZsyncFile
	packageSourceDir = args.packageSourceDir
	disablePigz = args.disablePigz

	if args.verbose:
		logLevel = LOG_DEBUG

	if logLevel != LOG_WARNING:
		logger.setColor(True)

	if quiet:
		logLevel = LOG_NONE

	logger.setConsoleLevel(logLevel)

	logger.info(u"Source dir: %s" % packageSourceDir)
	logger.info(u"Temp dir: %s" % tempDir)
	logger.info(u"Custom name: %s" % customName)
	logger.info(u"Archive format: %s" % format)

	if format not in ['tar', 'cpio']:
		raise Exception(u"Unsupported archive format: %s" % format)

	if not os.path.isdir(packageSourceDir):
		raise Exception(u"No such directory: %s" % packageSourceDir)

	if customName:
		packageControlFilePath = os.path.join(packageSourceDir, u'OPSI.%s' % customName, u'control')
	if not customName or not os.path.exists(packageControlFilePath):
		packageControlFilePath = os.path.join(packageSourceDir, u'OPSI', u'control')
		if not os.path.exists(packageControlFilePath):
			raise Exception(u"Control file '%s' not found" % packageControlFilePath)

	if not quiet:
		print ""
		print _(u"Locking package")
	pcf = PackageControlFile(packageControlFilePath, opsi3compatible=compatibilityMode)

	lockPackage(tempDir, pcf)
	pps = None
	try:
		while True:
			product = pcf.getProduct()
			if requiredVersion:
				match = re.search('^\s*([<>]?=?)(^[<>=]+)\s*$', requiredVersion)
				if not match:
					raise Exception(u"Bad version string '%s' in dependency" % requiredVersion)
				packageDependency = {
					'package': product.id,
					'condition': match.group(1),
					'version': match.group(2)
				}
				pcf.setPackageDependencies([packageDependency])
				pcf.setIncrementalPackage(True)

			if not quiet:
				print u""
				print _(u"Package info")
				print u"----------------------------------------------------------------------------"
				print u"   %-20s : %s" % (u'version', product.packageVersion)
				print u"   %-20s : %s" % (u'custom package name', customName)
				print u"   %-20s : %s" % (u'incremental package', bool(requiredVersion))

				dependencies = []
				for dep in pcf.getPackageDependencies():
					dependencies.append(u'%s(%s%s)' % (dep['package'], dep['condition'], dep['version']))
				print u"   %-20s : %s" % (u'package dependencies', u', '.join(dependencies))

				print ""
				print _(u"Product info")
				print u"----------------------------------------------------------------------------"
				print u"   %-20s : %s" % (u'product id', product.id)

				if product.getType() == 'LocalbootProduct':
					print u"   %-20s : %s" % (u'product type', u'localboot')
				elif product.getType() == 'NetbootProduct':
					print u"   %-20s : %s" % (u'product type', u'netboot')

				print u"   %-20s : %s" % (u'version', product.productVersion)
				print u"   %-20s : %s" % (u'name', product.name)
				print u"   %-20s : %s" % (u'description', product.description)
				print u"   %-20s : %s" % (u'advice', product.advice)
				print u"   %-20s : %s" % (u'priority', product.priority)
				print u"   %-20s : %s" % (u'licenseRequired', product.licenseRequired)
				print u"   %-20s : %s" % (u'product classes', u', '.join(product.productClassIds))
				print u"   %-20s : %s" % (u'windows software ids', u', '.join(product.windowsSoftwareIds))

				if product.getType() == 'NetbootProduct':
					print u"   %-20s : %s" % (u'pxe config template', product.pxeConfigTemplate)

				print ""
				print _(u"Product scripts")
				print u"----------------------------------------------------------------------------"
				print u"   %-20s : %s" % (u'setup', product.setupScript)
				print u"   %-20s : %s" % (u'uninstall', product.uninstallScript)
				print u"   %-20s : %s" % (u'update', product.updateScript)
				print u"   %-20s : %s" % (u'always', product.alwaysScript)
				print u"   %-20s : %s" % (u'once', product.onceScript)
				print u"   %-20s : %s" % (u'custom', product.customScript)
				if product.getType() == 'LocalbootProduct':
					print u"   %-20s : %s" % (u'user login', product.userLoginScript)
				print u""

			if disablePigz:
				logger.debug("Disabling pigz")
				OPSI.Util.File.Archive.PIGZ_ENABLED = False

			pps = ProductPackageSource(
				packageSourceDir=packageSourceDir,
				tempDir=tempDir,
				customName=customName,
				customOnly=customOnly,
				packageFileDestDir=os.getcwd(),
				format=format,
				compression=compression,
				dereference=dereference
			)

			if not quiet and os.path.exists(pps.getPackageFile()):
				print _(u"Package file '%s' already exists.") % pps.getPackageFile()
				print _(u"Press <O> to overwrite, <C> to abort or <N> to specify a new version:"),
				newVersion = False
				fd = sys.stdin.fileno()
				savedSettings = termios.tcgetattr(fd)
				tty.setraw(fd)
				try:
					while True:
						ch = sys.stdin.read(1)
						if ch in ('o', 'O'):
							if os.path.exists(pps.packageFile + u'.md5'):
								os.remove(pps.packageFile + u'.md5')
							if os.path.exists(pps.packageFile + u'.zsync'):
								os.remove(pps.packageFile + u'.zsync')
							break
						elif ch in ('c', 'C'):
							raise Exception(_(u"Aborted"))
						elif ch in ('n', 'N'):
							newVersion = True
							break
				finally:
					termios.tcsetattr(fd, termios.TCSADRAIN, savedSettings)
					print '\r\033[0K'

				if newVersion:
					while True:
						print '\r%s' % _(u"Please specify new product version, press <ENTER> to keep current version (%s):") % product.productVersion,
						newVersion = sys.stdin.readline().strip()
						try:
							if newVersion:
								product.setProductVersion(newVersion)
								pcf.generate()
							break
						except Exception:
							print _(u"Bad product version: %s") % newVersion

					while True:
						print '\r%s' % _(u"Please specify new package version, press <ENTER> to keep current version (%s):") % product.packageVersion,
						newVersion = sys.stdin.readline().strip()
						try:
							if newVersion:
								product.setPackageVersion(newVersion)
								pcf.generate()
							break
						except Exception:
							print _(u"Bad package version: %s") % newVersion
					continue

			# Regenerating to fix encoding
			pcf.generate()

			progressSubject = None
			if not quiet:
				progressSubject = ProgressSubject('packing')
				progressSubject.attachObserver(ProgressNotifier())
				print _(u"Creating package file '%s'") % pps.getPackageFile()
			pps.pack(progressSubject=progressSubject)

			if not quiet:
				print "\n"
			if createMd5SumFile:
				md5sumFile = u'%s.md5' % pps.getPackageFile()
				if not quiet:
					print _(u"Creating md5sum file '%s'") % md5sumFile
				md5 = md5sum(pps.getPackageFile())
				with open(md5sumFile, 'w') as f:
					f.write(md5)

			if createZsyncFile:
				zsyncFile = u'%s.zsync' % pps.getPackageFile()
				if not quiet:
					print _(u"Creating zsync file '%s'") % zsyncFile
				zsyncFile = ZsyncFile(zsyncFile)
				zsyncFile.generate(pps.getPackageFile())
			break
	finally:
		if pps:
			if not quiet:
				print _(u"Cleaning up")
			pps.cleanup()
		if not quiet:
			print _(u"Unlocking package")
		unlockPackage(tempDir, pcf)
		if not quiet:
			print ""


def lockPackage(tempDir, packageControlFile):
	lockFile = os.path.join(tempDir, u'.opsi-makeproductfile.lock.%s' % packageControlFile.getProduct().id)
	# Test if other processes are accessing same product
	try:
		with open(lockFile, 'r') as lf:
			p = lf.read().strip()

		if p:
			for line in execute(u"ps -A"):
				line = line.strip()
				if not line:
					continue
				if p == line.split()[0].strip():
					pName = line.split()[-1].strip()
					# process is running
					raise Exception(u"Product '%s' is currently locked by process %s (%s)."
									% (packageControlFile.getProduct().id, pName, p))

	except IOError:
		pass

	# Write lock-file
	with open(lockFile, 'w') as lf:
		lf.write(str(os.getpid()))


def unlockPackage(tempDir, packageControlFile):
	lockFile = os.path.join(tempDir, u'.opsi-makeproductfile.lock.%s' % packageControlFile.getProduct().id)
	if os.path.isfile(lockFile):
		os.unlink(lockFile)


if (__name__ == "__main__"):
	exception = None

	try:
		main(sys.argv[1:])
	except SystemExit:
		pass
	except Exception as exception:
		logger.setConsoleLevel(LOG_ERROR)
		logger.logException(exception)
		print >> sys.stderr, u"ERROR: %s" % exception
		sys.exit(1)

	sys.exit(0)
