#! /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-2015 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 getopt
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 forceInt, 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

__version__ = '4.0.6.3'

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


def usage():
	print u"\nUsage: %s [-h] [-z] [-m] [-v|-q] [-F format] [-l log-level] [-i|-c custom name] [-I required version] [-t temp dir] [source directory]" % os.path.basename(sys.argv[0])
	print u"Provides an opsi package from a package source directory."
	print u"If no source directory is supplied, the current directory will be used."
	print u"Options:"
	print u"   -v          verbose"
	print u"   -q          quiet"
	print u"   -l          log-level 0..9"
	print u"   -n          do not compress"
	print u"   -F          archive format [tar|cpio], default: cpio"
	print u"   -h          follow symlinks"
	print u"   -I          incremental package"
	print u"   -i          custom name (add custom files)"
	print u"   -c          custom name (custom only)"
	print u"   -C          compatibility mode (opsi 3)"
	print u"   -t          temp dir"
	print u"   -m          create md5sum file"
	print u"   -z          create zsync file"
	print u"   --no-pigz   Disable the usage of pigz"
	print u""


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)

	logLevel = LOG_WARNING
	packageSourceDir = os.getcwd()
	tempDir = u'/tmp'
	customName = u''
	customOnly = False
	quiet = False
	format = 'cpio'
	compression = 'gzip'
	disablePigz = False
	dereference = False
	requiredVersion = None
	incremental = False
	compatibilityMode = False
	createMd5SumFile = False
	createZsyncFile = False

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

	try:
		(opts, args) = getopt.getopt(argv, "Chvzmnqi:c:t:l:F:I:", ['no-pigz'])
	except getopt.GetoptError:
		usage()
		sys.exit(1)

	for (opt, arg) in opts:
		if (opt == "-h"):
			dereference = True
		elif (opt == "-v"):
			logLevel = LOG_DEBUG
			logger.setColor(True)
		elif (opt == "-l"):
			logLevel = forceInt(arg)
			logger.setColor(True)
		elif (opt == "-n"):
			compression = None
		elif (opt == "-q"):
			logLevel = LOG_NONE
			quiet = True
		elif (opt == "-i"):
			customName = arg
		elif (opt == "-c"):
			customName = arg
			customOnly = True
		elif (opt == "-I"):
			requiredVersion = arg
			incremental = True
		elif (opt == "-t"):
			tempDir = forceFilename(arg)
		elif (opt == "-F"):
			format = forceUnicode(arg)
		elif (opt == "-C"):
			compatibilityMode = True
		elif (opt == "-m"):
			createMd5SumFile = True
		elif (opt == "-z"):
			createZsyncFile = True
		elif opt == "--no-pigz":
			disablePigz = True

	if (len(args) > 1):
		logger.error(u"Too many arguments")
		usage()
		sys.exit(1)

	elif (len(args) > 0):
		packageSourceDir = os.path.abspath( args[0] )

	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 incremental:
				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', incremental)

				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)
