#!/usr/bin/python2.7
# -*- coding: utf-8 -*-
#
# UCS Virtual Machine Manager Daemon
#  UVMM handler
#
# Copyright 2010-2018 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/>.
"""UVMM Daemon."""

import sys
import locale
import ConfigParser
import logging
import logging.config
from optparse import OptionParser, OptionGroup
from univention.uvmm.helpers import TranslatableException, _, N_
import os
import signal
import libvirt
from threading import Thread
from time import time


def createDaemon(options):
	"""http://code.activestate.com/recipes/278731/"""
	UMASK = 0o037
	WORKDIR = "/"
	MAXFD = 1024

	try:
		(read_end, write_end) = os.pipe()
		pid = os.fork()
	except OSError as e:
		raise TranslatableException(N_("%(strerror)s [%(errno)d]"), stderror=e.strerror, errno=e.errno)
	if (pid == 0):  # 1st child
		# detach from parent environment
		os.chdir(WORKDIR)
		os.setsid()  # Become new session leader
		os.umask(UMASK)

		try:
			pid = os.fork()
		except OSError as e:
			raise TranslatableException(N_("%(strerror)s [%(errno)d]"), stderror=e.strerror, errno=e.errno)
		if (pid == 0):  # 2nd child
			os.close(read_end)

			def server_ready():
				"""Notify parent that server is ready."""
				pid = os.getpid()
				os.write(write_end, str(pid))
				os.close(write_end)
			options.callback_ready = server_ready

			import resource
			maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
			if (maxfd == resource.RLIM_INFINITY):
				maxfd = MAXFD

			dev_null = file(os.path.devnull, 'r')
			try:
				os.dup2(dev_null.fileno(), sys.stdin.fileno())
			finally:
				dev_null.close()
			out_log = open(options.logfile or os.path.devnull, 'a+')
			try:
				os.dup2(out_log.fileno(), sys.stdout.fileno())
				os.dup2(out_log.fileno(), sys.stderr.fileno())
			finally:
				out_log.close()
			keep_fd = (sys.stdin.fileno(), sys.stdout.fileno(), sys.stderr.fileno(), write_end)
			for fd in range(maxfd):
				try:
					if fd not in keep_fd:
						os.close(fd)
				except OSError:
					pass

			signal.signal(signal.SIGCHLD, signal.SIG_IGN)  # child
			signal.signal(signal.SIGTSTP, signal.SIG_IGN)  # terminal stop
			signal.signal(signal.SIGTTOU, signal.SIG_IGN)  # terminal text out
			signal.signal(signal.SIGTTIN, signal.SIG_IGN)  # termianl text in
		else:  # 1st child
			os._exit(0)
	else:  # parent
		try:
			os.close(write_end)
			pid = os.read(read_end, 11)
			os.close(read_end)

			if pid:
				fd = open(options.pidfile, "w")
				try:
					fd.write(pid)
				finally:
					fd.close()
				sys.exit(0)
			else:
				print >>sys.stderr, _("Failed to fork daemon.")
				sys.exit(1)
		except OSError as e:
			raise TranslatableException(N_("%(strerror)s [%(errno)d]"), stderror=e.strerror, errno=e.errno)


def install_signal_handlers(options):
	u"""Install signal handlers."""

	def signal_handler_restart(signum, frame):
		"""Handle HUP signal to restart daemon."""
		self = os.path.abspath(sys.argv[0])
		args = sys.argv[:]
		logger.info("Re-executing self: %s %s" % (self, args))
		os.execv(self, args)
		logger.error("Failed to re-executing self.")
		sys.exit(1)

	def signal_handler_logrotate(signum, frame):
		"""Handle USR1 signal to re-open logfile."""
		logger = logging.getLogger("uvmmd")
		logger.info('Closing logfile.')
		# Inspired by <http://www.cherrypy.org/ticket/679>
		for x in logger.manager.loggerDict.itervalues():
			try:
				for h in x.handlers:
					if isinstance(h, logging.FileHandler):
						h.acquire()
						try:
							h.stream.close()
							h.stream = open(h.baseFilename, h.mode)
						finally:
							h.release()
			except AttributeError:
				pass
		logger.info('Logfile reopened.')
		# Re-open STDOUT, STDERR logfile
		if options.logfile:
			out_log = open(options.logfile, 'a+')
			try:
				os.dup2(out_log.fileno(), sys.stdout.fileno())
				os.dup2(out_log.fileno(), sys.stderr.fileno())
			finally:
				out_log.close()

	def signal_handler_terminate(signum, frame):
		"""Handle TERM signal to cleanup before terminate."""
		if os.path.exists(options.pidfile):
			os.remove(options.pidfile)
		if os.path.exists(options.socket):
			os.remove(options.socket)
		signal.signal(signum, signal.SIG_DFL)
		os.kill(os.getpid(), signum)

	signal.signal(signal.SIGHUP, signal_handler_logrotate)  # hang up
	signal.signal(signal.SIGUSR1, signal_handler_restart)  # user1
	signal.signal(signal.SIGINT, signal_handler_terminate)  # interrupt
	signal.signal(signal.SIGTERM, signal_handler_terminate)  # terminate
	signal.signal(signal.SIGSEGV, signal_handler_terminate)  # memory corrupt

	try:
		from meliae import scanner

		def signal_handler_dump(signum, frame):
			"""Handle SIGUSR2 to dump memory usage"""
			filename = '/var/tmp/uvmm-mm_%d.json' % (time(),)
			scanner.dump_all_objects(filename)
		signal.signal(signal.SIGUSR2, signal_handler_dump)  # user2
	except ImportError:
		signal.signal(signal.SIGUSR2, signal.SIG_IGN)  # user2


def runEventLoop():
	while True:
		libvirt.virEventRunDefaultImpl()


if __name__ == '__main__':
	locale.setlocale(locale.LC_ALL, '')

	parser = OptionParser(usage=_('usage: %prog [options] [uri...]'))

	socket_group = OptionGroup(parser, _('Connection options'))
	socket_group.add_option('-u', '--unix',
				action='store', dest='socket', default="/var/run/uvmm.socket",
				help=_('Path to the UNIX socket [%(default)s]') % {'default': '%default'})
	parser.add_option_group(socket_group)

	cache_group = OptionGroup(parser, _('Cache options'))
	cache_group.add_option('-c', '--cache',
			action='store', dest='cache', default="/var/cache/univention-virtual-machine-manager-daemon",
			help=_('cache directory [%(default)s]') % {'default': '%default'})
	cache_group.add_option('-C', '--clean',
			action='store_true', dest='clean', default=False,
			help=_('Do not load cached state'))
	parser.add_option_group(cache_group)

	daemon_group = OptionGroup(parser, _('Daemon options'))
	daemon_group.add_option('-p', '--pidfile',
			action='store', dest='pidfile', default="/var/run/uvmmd.pid",
			help=_('Path to the pid-file [%(default)s]') % {'default': '%default'})
	daemon_group.add_option('-d', '--daemon',
			action='store_true', dest='daemonize', default=False,
			help=_('Fork into background'))
	daemon_group.add_option('-l', '--log',
			action='store', dest='logfile', default=None,
			help=_('Path to the log-file'))
	daemon_group.add_option('-v', '--verbose',
			action='store_true', dest='verbose', default=False,
			help=_('Print additional information'))
	daemon_group.add_option('-i', '--config',
			action='store', dest='conffile', default="/etc/univention/uvmmd.ini",
			help=_('Path to the ini-file [%(default)s]') % {'default': '%default'})
	daemon_group.add_option('-I', '--no-ini',
			action='store_false', dest='read_ini', default=True,
			help=_('Skip reading ini-file'))
	parser.add_option_group(daemon_group)

	(options, arguments) = parser.parse_args()

	if options.daemonize:
		createDaemon(options)

	# Silence libvirt to not log non-connection-related messages to stderr.
	def uvmm_libvirt_logger(ctx, error):
		pass
	libvirt.registerErrorHandler(uvmm_libvirt_logger, ctx=None)

	install_signal_handlers(options)

	config = ConfigParser.ConfigParser()
	if options.read_ini:
		config.read(options.conffile)
	logging.basicConfig(level=logging.CRITICAL, filename=options.logfile)
	try:
		logging.config.fileConfig(options.conffile)
		logger = logging.getLogger("uvmmd")
	except ConfigParser.Error as e:
		logger = logging.getLogger("uvmmd")
		logger.warning('Could not configure logger from %s' % (options.conffile,))

	if options.verbose:
		logger.setLevel(logging.DEBUG)

	# Create global event loop
	libvirt.virEventRegisterDefaultImpl()
	loop = Thread(target=runEventLoop, name="libvirtEventLoop")
	loop.setDaemon(True)
	loop.start()

	from univention.uvmm.node import node_add, nodes
	if not options.clean and options.cache != '':
		nodes.set_cache(options.cache)

	# Add nodes from .ini file
	for section in config.sections():
		if section.startswith("poller:"):
			try:
				uri = config.get(section, "uri")
				node_add(uri)
			except ConfigParser.NoOptionError:
				logger.warning("Section [%s] has no uri." % (section,))

	# Add nodes from command line
	for uri in arguments:
		node_add(uri)

	# Add cloud connections
	from univention.uvmm.cloudnode import cloudconnections
	if not options.clean and options.cache != '':
		cloudconnections.set_cache(options.cache)

	# Add nodes from LDAP
	from univention.uvmm.uvmm_ldap import cached, ldap_uris, LdapError, ldap_cloud_connections
	try:
		if not options.clean and options.cache != '':
			def get_ldap_uris():
				return ldap_uris()
			uris = cached("%s/ldap.pic" % (options.cache,), get_ldap_uris)
		else:
			uris = ldap_uris()

		for uri in uris:
			node_add(uri)

		for connection in ldap_cloud_connections():
			try:
				cloudconnections.add_connection(connection, testconnection=False)
			except:
				logger.error("Could not add connection %s (from ldap)" % (connection["name"]))

	except LdapError as msg:
		logger.warning(msg)

	from univention.uvmm.unix import unix
	unix(options)
