#! /usr/bin/python
# -*- coding: utf-8 -*-
"""
opsi pxe configuration daemon (opsipxeconfd)

opsiconfd is part of the desktop management solution opsi
(open pc server integration) http://www.opsi.org

Copyright (C) 2013-2016 uib GmbH

http://www.uib.de/

All rights reserved.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License, version 3
as published by the Free Software Foundation.

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 General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

@copyright:	uib GmbH <info@uib.de>
@author: Erol Ueluekmen <e.ueluekmen@uib.de>
@author: Jan Schneider <j.schneider@uib.de>
@author: Niko Wenselowski <n.wenselowski@uib.de>
@license: GNU Affero GPL version 3
"""

import base64
import getopt
import os
import socket
import stat
import sys
import threading
import time
from signal import SIGHUP, SIGINT, SIGTERM, signal
from twisted.conch.ssh import keys
from hashlib import md5

from OPSI.Backend.BackendManager import BackendManager
from OPSI.Logger import LOG_NONE, LOG_NOTICE, LOG_WARNING, Logger
from OPSI.System.Posix import execute, which
from OPSI.Util import getfqdn
from OPSI.Util.File import ConfigFile
from OPSI.Types import (forceFilename, forceHostId, forceInt, forceUnicode, forceUnicodeList)

__version__ = '4.0.7.1'

logger = Logger()


class Opsipxeconfd(threading.Thread):
	def __init__(self, config):
		threading.Thread.__init__(self)

		self.config = config
		self._running = False

		self._backend = None
		self._socket = None
		self._clientConnectionLock = threading.Lock()
		self._pxeConfigWritersLock = threading.Lock()
		self._clientConnections = []
		self._pxeConfigWriters = []
		self._startupTask = None

		self._setOpsiLogging()
		logger.comment("""\
==================================================================
=           opsi pxe configuration service starting              =
==================================================================""")

	def setConfig(self, config):
		logger.notice(u"Got new config")
		self.config = config

	def isRunning(self):
		return self._running

	def stop(self):
		logger.notice(u"Stopping opsipxeconfd main thread")
		if self._startupTask:
			self._startupTask.stop()
			self._startupTask.join(10)
		try:
			self._socket.close()
		except Exception as error:
			logger.error(u"Failed to close socket: %s" % error)
		self._running = False

	def reload(self):
		logger.notice(u"Reloading opsipxeconfd")
		self._setOpsiLogging()
		self._createBackendInstance()
		self._createSocket()

	def _setOpsiLogging(self):
		# Set logging options
		if self.config['logFile']:
			logger.setLogFile(self.config['logFile'])
		if self.config['logFormat']:
			logger.setLogFormat(self.config['logFormat'])
		logger.setFileLevel(self.config['logLevel'])

	def _createBackendInstance(self):
		logger.info(u"Creating backend instance")
		self._backend = BackendManager(
			dispatchConfigFile=self.config['dispatchConfigFile'],
			dispatchIgnoreModules=['OpsiPXEConfd'],  # Avoid loops
			backendConfigDir=self.config['backendConfigDir'],
			extend=True
		)
		self._backend.backend_setOptions({'addProductPropertyStateDefaults': True, 'addConfigStateDefaults': True})

	def _createSocket(self):
		return self._createUnixSocket()

	def _createUnixSocket(self):
		logger.notice(u"Creating unix socket '%s'" % self.config['port'])
		if os.path.exists(self.config['port']):
			os.unlink(self.config['port'])
		self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
		try:
			self._socket.bind(self.config['port'])
		except Exception as error:
			raise Exception(u"Failed to bind to socket '%s': %s" % (self.config['port'], error))
		self._socket.settimeout(0.1)
		self._socket.listen(self.config['maxConnections'])

		mode = os.stat(self.config['port'])[0]
		os.chmod(self.config['port'], mode | stat.S_IROTH | stat.S_IWOTH)

	def _getConnection(self):
		logger.debug2(u"_getConnection()")
		(addr, sock) = (None, None)
		try:
			(sock, addr) = self._socket.accept()
		except socket.error as error:
			if error.args[0] == 'timed out' or error.args[0] == 11:
				return
			raise
		logger.notice(u"Got connection from client")

		cc = None
		try:
			logger.info(u"Creating thread for connection %s" % (len(self._clientConnections) + 1))
			cc = ClientConnection(self, sock, self.clientConnectionCallback)
			with self._clientConnectionLock:
				self._clientConnections.append(cc)
			cc.start()
		except Exception as error:
			logger.error(u"Failed to create control connection: %s" % error)
			logger.logException(error)

			with self._clientConnectionLock:
				try:
					self._clientConnections.remove(cc)
				except ValueError:
					pass  # Element not in list

	def run(self):
		self._running = True
		logger.notice(u"Starting opsipxeconfd main thread")
		try:
			self._createBackendInstance()
			logger.info("Setting needed boot configurations")
			self._startupTask = StartupTask(self)
			self._startupTask.start()
			self._createSocket()
			while self._running:
				self._getConnection()
			logger.notice(u"Opsipxeconfd main thread exiting...")
		except Exception as error:
			logger.logException(error)
		self._running = False

	def clientConnectionCallback(self, cc):
		logger.info(u"ClientConnection '%s' finished (took %0.3f seconds)" % (cc.getName(), (time.time() - cc.startTime)))

		try:
			with self._clientConnectionLock:
				try:
					self._clientConnections.remove(cc)
				except ValueError:
					pass  # Connection not in list

			logger.debug(u"ClientConnection removed")
		except Exception as error:
			logger.error(u"Failed to remove ClientConnection: %s" % error)

	def pxeConfigWriterCallback(self, pcw):
		logger.info(u"PXEConfigWriter '%s' finished (took %0.3f seconds)" % (pcw.getName(), (time.time() - pcw.startTime)))

		try:
			with self._pxeConfigWritersLock:
				try:
					self._pxeConfigWriters.remove(pcw)
				except ValueError:
					pass  # Writer not in list
			logger.debug(u"PXE config writer removed")
		except Exception as error:
			logger.error(u"Failed to remove PXE config writer: %s" % error)

		gotAlways = False
		for i, poc in enumerate(pcw.productOnClients):
			# renew objects and check if anythin changes on service since callback
			productOnClients = self._backend.productOnClient_getObjects(
				productType=u'NetbootProduct',
				clientId=poc.clientId,
				productId=poc.productId
			)
			if productOnClients:
				pcw.productOnClients[i] = productOnClients[0]
			else:
				del pcw.productOnClients[i]

			if pcw.productOnClients:
				pcw.productOnClients[i].setActionProgress(u'pxe boot configuration read')
				if pcw.productOnClients[i].getActionRequest() == u'always':
					gotAlways = True
				if pcw.templatefile != self.config['pxeConfTemplate'] and not gotAlways:
					pcw.productOnClients[i].setActionRequest(u'none')

		if pcw.productOnClients:
			self._backend.productOnClient_updateObjects(pcw.productOnClients)
		if gotAlways:
			self.updateBootConfiguration(pcw.hostId)

	def status(self):
		logger.notice(u"Getting opsipxeconfd status")
		result = u'opsipxeconfd status:\n'

		with self._clientConnectionLock:
			result += u'%s control connection(s) established\n' % len(self._clientConnections)
			for i, connection in enumerate(self._clientConnections, start=1):
				result += u'    Connection %s established at: %s\n' \
					% (i, time.asctime(time.localtime(connection.startTime)))

		result += u'\n%s boot configuration(s) set\n' % len(self._pxeConfigWriters)
		for pcw in self._pxeConfigWriters:
			result += u"Boot config for client '%s', args %s, path '%s' set at: %s\n" \
				% (	pcw.hostId,
					pcw.args,
					pcw.pxefile,
					time.asctime(time.localtime(pcw.startTime)))
		logger.notice(result)
		return result

	def updateBootConfiguration(self, hostId):
		try:
			hostId = forceHostId(hostId)
			logger.info(u"Updating PXE boot configuration for host '%s'" % hostId)

			# Remove current pxe config for host
			currentPcws = []
			with self._pxeConfigWritersLock:
				for pcw in self._pxeConfigWriters:
					if pcw.hostId == hostId:
						currentPcws.append(pcw)

				for pcw in currentPcws:
					self._pxeConfigWriters.remove(pcw)

			for pcw in currentPcws:
				pcw.stop()
				while pcw.isAlive():
					time.sleep(0.1)
				logger.notice(u"PXE boot configuration for host '%s' removed" % hostId)

			# Get product actions
			productOnClients = self._backend.productOnClient_getObjects(
				productType=u'NetbootProduct',
				clientId=hostId,
				actionRequest=['setup', 'uninstall', 'update', 'always', 'once', 'custom']
			)

			if not productOnClients:
				return u'Boot configuration updated'

			# Get host
			host = self._backend.host_getObjects(id=hostId)
			if not host:
				raise Exception(u"Host '%s' not found" % hostId)
			host = host[0]

			newProductOnClients = []
			for poc in productOnClients:
				productOnDepot = self._backend.productOnDepot_getObjects(
					productType=u'NetbootProduct',
					productId=poc.productId,
					depotId=self.config['depotId']
				)
				if not productOnDepot:
					logger.info(u"Product %s not available on depot '%s'" % (poc.productId, self.config['depotId']))
					continue
				poc.productVersion = productOnDepot[0].productVersion
				poc.packageVersion = productOnDepot[0].packageVersion
				newProductOnClients.append(poc)
			productOnClients = newProductOnClients

			if not productOnClients:
				return u'Boot configuration updated'

			self._backend.backend_setOptions({'addProductPropertyStateDefaults': True, 'addConfigStateDefaults': True})

			# Try to detect elilo mode
			eliloMode = False
			eliloX86Mode = False
			configStates = self._backend.configState_getObjects(configId="clientconfig.dhcpd.filename", objectId=hostId)
			if configStates:
				val = configStates[0].getValues()
				if val:
					if val and 'elilo' in val[0]:
						if 'x86' in val[0]:
							eliloX86Mode = True
						else:
							eliloMode = True

			# Get pxe config template
			pxeConfigTemplate = None
			for poc in productOnClients:
				product = self._backend.product_getObjects(
					type=u'NetbootProduct',
					id=poc.productId,
					productVersion=poc.productVersion,
					packageVersion=poc.packageVersion
				)
				if not product:
					raise Exception(u"Product not found: %s_%s-%s" % (poc.productId, poc.productVersion, poc.packageVersion))
				product = product[0]

				if eliloMode:
					pxeConfigTemplate = self.config['uefiConfTemplate-x64']
				elif eliloX86Mode:
					pxeConfigTemplate = self.config['uefiConfTemplate-x86']

				if product.pxeConfigTemplate:
					if not eliloMode:
						if pxeConfigTemplate and (pxeConfigTemplate != product.pxeConfigTemplate):
							logger.error(u"Cannot use more than one pxe config template, got: %s, %s" \
								% (pxeConfigTemplate, product.pxeConfigTemplate))
						else:
							pxeConfigTemplate = product.pxeConfigTemplate
							logger.notice(u"Special pxe config template '%s' will be used used for host '%s', product '%s'" \
											% (pxeConfigTemplate, hostId, poc.productId))
					else:
						logger.notice("Ignoring given pceConfigTemplate because uefi detected for the client.")

			if not pxeConfigTemplate:
				pxeConfigTemplate = self.config['pxeConfTemplate']

			if not os.path.isabs(pxeConfigTemplate):
				# Not an absolute path
				pxeConfigTemplate = os.path.join(os.path.dirname(self.config['pxeConfTemplate']), pxeConfigTemplate)

			logger.debug(u"Using pxe config template '%s'" % pxeConfigTemplate)

			# Get name for PXE config file
			pxeConfigName = ''
			if host.getHardwareAddress():
				logger.debug(u"Got hardware address '%s' for host '%s'" % (host.getHardwareAddress(), hostId))
				pxeConfigName = u"01-%s" % host.getHardwareAddress().replace(u':', u'-')
			elif host.getIpAddress():
				logger.warning(u"Failed to get hardware address for host '%s', using ip address '%s'" % (hostId, host.getIpAddress()))
				pxeConfigName = '%02X%02X%02X%02X' % tuple([int(i) for i in host.getIpAddress().split('.')])
			else:
				raise Exception(u"Neither hardware address nor ip address known for host '%s'" % hostId)
			pxefile = os.path.join(self.config['pxeDir'], pxeConfigName)
			if os.path.exists(pxefile):
				for pcw in self._pxeConfigWriters:
					if pcw.uefi and not pcw._uefiModule:
						raise Exception(u"Should use uefi netboot, but uefi module is not licensed.")
					if pcw.pxefile == pxefile:
						if host.id == pcw.hostId:
							logger.notice(u"PXE boot configuration '%s' for client '%s' already exists." % (pxefile, host.id))
							return
						else:
							raise Exception(u"PXE boot configuration '%s' already exists. Clients '%s' and '%s' using same address?" \
									% (pxefile, host.id, pcw.hostId))
				logger.debug(u"PXE boot configuration '%s' already exists, removing." % pxefile)
				os.unlink(pxefile)

			# Append arguments
			append = {
				'pckey': host.getOpsiHostKey(),
				'hn': hostId.split('.')[0],
				'dn': u'.'.join(hostId.split('.')[1:]),
				'product': product.id
			}
			if append['pckey']:
				logger.addConfidentialString(append['pckey'])

			# Get config service id
			append['service'] = u''
			configStates = self._backend.configState_getObjects(objectId=hostId, configId=u'clientconfig.configserver.url')
			if configStates:
				append['service'] = configStates[0].getValues()[0]
			if not append['service'].endswith(u'/rpc'):
				append['service'] += u'/rpc'

			# Get additional bootimage append params
			configStates = self._backend.configState_getObjects(objectId=hostId, configId=u'opsi-linux-bootimage.append')
			if configStates:
				app = u' '.join(forceUnicodeList(configStates[0].getValues()))
				for option in app.split():
					keyValue = option.split(u"=")
					if len(keyValue) < 2:
						append[keyValue[0].strip().lower()] = u''
					else:
						append[keyValue[0].strip().lower()] = keyValue[1].strip()

			# Get product property states
			productPropertyStates = {}
			productIds = []
			for poc in productOnClients:
				productIds.append(poc.productId)
			if productIds:
				for pps in self._backend.productPropertyState_getObjects(objectId=hostId, productId=productIds):
					productPropertyStates[pps.propertyId] = u','.join(forceUnicodeList(pps.getValues()))

			pcw = None
			try:
				logger.info(u"Creating thread for pxeconfig %s" % (len(self._pxeConfigWriters) + 1))
				backendinfo = self._backend.backend_info()
				backendinfo['hostCount'] = len(self._backend.host_getIdents(type='OpsiClient'))
				pcw = PXEConfigWriter(pxeConfigTemplate, hostId, productOnClients, append, productPropertyStates, pxefile, self.pxeConfigWriterCallback, backendinfo)
				with self._pxeConfigWritersLock:
					self._pxeConfigWriters.append(pcw)
				pcw.start()
				logger.notice(u"PXE boot configuration for host %s is now set at '%s'" % (hostId, pxefile))
				return u'Boot configuration updated'
			except Exception as error:
				logger.error(u"Failed to create pxe config writer: %s" % error)

				with self._pxeConfigWritersLock:
					try:
						self._pxeConfigWriters.remove(pcw)
					except ValueError:
						pass  # Writer not in list

				raise
		except Exception as error:
			logger.logException(error)
			raise


class StartupTask(threading.Thread):
	def __init__(self, opsipxeconfd):
		threading.Thread.__init__(self)
		self._opsipxeconfd = opsipxeconfd
		self._running = False
		self._stop = False

	def run(self):
		self._running = True
		try:
			logger.notice(u"Start setting needed boot configurations")

			clientIds = []
			for clientToDepot in self._opsipxeconfd._backend.configState_getClientToDepotserver(depotIds=[self._opsipxeconfd.config['depotId']]):
				clientIds.append(clientToDepot['clientId'])

			if clientIds:
				productOnClients = self._opsipxeconfd._backend.productOnClient_getObjects(
					productType=u'NetbootProduct',
					clientId=clientIds,
					actionRequest=['setup', 'uninstall', 'update', 'always', 'once', 'custom']
				)

				clientIds = []
				for poc in productOnClients:
					if not poc.clientId in clientIds:
						clientIds.append(poc.clientId)

				for clientId in clientIds:
					if self._stop:
						return

					try:
						self._opsipxeconfd.updateBootConfiguration(clientId)
					except Exception as error:
						logger.error(u"Failed to update PXE boot config for client '%s': %s" % (clientId, error))

			logger.notice(u"Finished setting needed boot configurations")
		except Exception as error:
			logger.logException(error)
		self._running = False

	def stop(self):
		self._stop = True


class PXEConfigWriter(threading.Thread):
	def __init__(self, templatefile, hostId, productOnClients, append, productPropertyStates, pxefile, callback=None, backendinfo={}):
		threading.Thread.__init__(self)
		self.templatefile = templatefile
		self.append = append
		self.productPropertyStates = productPropertyStates
		self.hostId = hostId
		self.productOnClients = productOnClients
		self.pxefile = pxefile
		self.startTime = time.time()
		self._callback = callback
		self._pipe = None
		self._uefiModule = False
		self.uefi = False

		if backendinfo:
			modules = backendinfo['modules']
			helpermodules = backendinfo['realmodules']
			hostCount = backendinfo['hostCount']

			if modules.get('customer'):
				publicKey = keys.Key.fromString(data=base64.decodestring('AAAAB3NzaC1yc2EAAAADAQABAAABAQCAD/I79Jd0eKwwfuVwh5B2z+S8aV0C5suItJa18RrYip+d4P0ogzqoCfOoVWtDojY96FDYv+2d73LsoOckHCnuh55GA0mtuVMWdXNZIE8Avt/RzbEoYGo/H0weuga7I8PuQNC/nyS8w3W8TH4pt+ZCjZZoX8S+IizWCYwfqYoYTMLgB0i+6TCAfJj3mNgCrDZkQ24+rOFS4a8RrjamEz/b81noWl9IntllK1hySkR+LbulfTGALHgHkDUlk0OSu+zBPw/hcDSOMiDQvvHfmR4quGyLPbQ2FOVm1TzE0bQPR+Bhx4V8Eo2kNYstG2eJELrz7J1TJI0rCjpB+FQjYPsP')).keyObject
				data = u''
				mks = modules.keys()
				mks.sort()
				for module in mks:
					if module in ('valid', 'signature'):
						continue
					if helpermodules.has_key(module):
						val = helpermodules[module]
						if module == 'uefi':
							if int(val) + 50 <= hostCount:
								logger.error(u"UNDERLICENSED: You have more Clients then licensed in modules file.  Disabling module: '%s'" % module)
								modules[module] = False
							elif int(val) <= hostCount:
								logger.warning("UNDERLICENSED WARNING: You have more Clients then licensed in modules file.")
						else:
							if int(val) > 0:
								modules[module] = True
					else:
						val = modules[module]
						if val == False:
							val = 'no'
						if val == True:
							val = 'yes'
					data += u'%s = %s\r\n' % (module.lower().strip(), val)
				if not bool(publicKey.verify(md5(data).digest(), [long(modules['signature'])])):
					logger.error(u"Failed to verify modules signature")
					return
				else:
					if modules.get('uefi'):
						self._uefiModule = True

		logger.info(u"PXEConfigWriter initializing: templatefile '%s', pxefile '%s', hostId '%s', append %s" \
				% (self.templatefile, self.pxefile, self.hostId, self.append))

		if not os.path.exists(self.templatefile):
			raise Exception(u"Template file '%s' not found" % self.templatefile)

		logger.debug(u"Reading Template")
		with open(self.templatefile, 'r') as infile:
			templateLines = infile.readlines()

		self.template = {'pxelinux': []}

		# Set pxe config content
		self.content = u''
		appendLineProperties = []

		for line in templateLines:
			line = line.rstrip()

			for (propertyId, value) in self.productPropertyStates.items():
				logger.debug("Property: '%s': value: '%s'" % (propertyId, value))
				line = line.replace(u'%%%s%%' % propertyId, value)

			if line.lstrip().startswith(u'append'):
				if line.lstrip().startswith(u'append='):
					logger.notice("elilo configuration detected")
					self.uefi = True
					appendLineProperties = ''.join(line.split('="')[1:])[:-1].split()
				else:
					self.uefi = False
					appendLineProperties = line.lstrip().split()[1:]

				for key, value in self.append.items():
					if value:
						appendLineProperties.append("%s=%s" % (key, value))
					else:
						appendLineProperties.append(str(key))

				if self._uefiModule and self.uefi:
					self.content = '%sappend="%s"\n' % (self.content, ' '.join(appendLineProperties))
				elif not self._uefiModule and self.uefi:
					self.contect = ""
					raise Exception(u"You have not licensed uefi module, please check your modules or contact info@uib.de")
				else:
					self.content = '%s  append %s\n' % (self.content, ' '.join(appendLineProperties))
			else:
				self.content = "%s%s\n" % (self.content, line)

		if 'pckey' in append:
			del self.append['pckey']

		if os.path.exists(self.pxefile):
			os.unlink(self.pxefile)
		os.mkfifo(self.pxefile)
		os.chmod(self.pxefile, 0644)

	def run(self):
		self._running = True
		pipeOpenend = False
		while self._running and not pipeOpenend:
			try:
				self._pipe = os.open(self.pxefile, os.O_WRONLY | os.O_NONBLOCK)
				pipeOpenend = True
			except Exception as error:
				if error.errno != 6:
					raise
				time.sleep(1)

		if pipeOpenend:
			logger.notice(u"Pipe '%s' opened, piping pxe boot configuration" % self.pxefile)
			os.write(self._pipe, self.content)
			os.close(self._pipe)

		if os.path.exists(self.pxefile):
			os.unlink(self.pxefile)

		if pipeOpenend and self._callback:
			self._callback(self)

	def stop(self):
		self._running = False


class ClientConnection(threading.Thread):
	def __init__(self, opsipxeconfd, socket, callback=None):
		threading.Thread.__init__(self)
		self._opsipxeconfd = opsipxeconfd
		self._socket = socket
		self._callback = callback
		self.startTime = time.time()

	def run(self):
		self._running = True
		self._socket.settimeout(1.0)

		cmd = self._socket.recv(4096)
		cmd = forceUnicode(cmd.strip())
		logger.info(u"Got command '%s'" % cmd)

		result = self._processCommand(cmd)
		logger.info(u"Returning result '%s'" % result)

		try:
			self._socket.send(result.encode('utf-8'))
		except Exception as error:
			logger.warning(error)

		self._socket.close()

		if self._running and self._callback:
			self._callback(self)

	def stop(self):
		self._running = False
		if self._socket:
			self._socket.close()

	def _processCommand(self, cmd):
		try:
			cp = cmd.split()

			if cp[0] == u'stop':
				self._opsipxeconfd.stop()
				return u'opsipxeconfd is going down'
			elif cp[0] == u'status':
				return self._opsipxeconfd.status()
			elif cp[0] == u'update':
				if len(cp) != 2:
					raise Exception(u"bad arguments for command 'update', needs <hostId>")
				hostId = forceHostId(cp[1])
				return self._opsipxeconfd.updateBootConfiguration(hostId)

			raise Exception(u"Command '%s' not supported" % cmd)
		except Exception as error:
			logger.error(error)
			return u"(ERROR): %s" % error


class ServerConnection:
	def __init__(self, port):
		self.port = port

	def createUnixSocket(self):
		logger.notice(u"Creating unix socket '%s'" % self.port)
		self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
		self._socket.settimeout(5.0)
		try:
			self._socket.connect(self.port)
		except Exception as error:
			raise Exception(u"Failed to connect to socket '%s': %s" % (self.port, error))

	def sendCommand(self, cmd):
		self.createUnixSocket()
		self._socket.send(forceUnicode(cmd).encode('utf-8'))
		result = None
		try:
			result = forceUnicode(self._socket.recv(4096))
		except Exception as error:
			raise Exception(u"Failed to receive: %s" % error)
		self._socket.close()
		if result.startswith(u'(ERROR)'):
			raise Exception(u"Command '%s' failed: %s" % (cmd, result))
		return result


class OpsipxeconfdInit(object):
	def __init__(self):
		logger.debug(u"OpsiPXEConfdInit")
		# Set umask
		os.umask(0077)
		self._pid = 0

		try:
			(self.opts, self.args) = getopt.getopt(sys.argv[1:], "vFl:c:", ["no-fork", "loglevel=", "conffile="])
		except getopt.GetoptError:
			self.usage()
			sys.exit(1)

		if len(self.args) < 1:
			self.usage()
			sys.exit(1)

		self.setDefaultConfig()
		# Process command line arguments
		for (opt, arg) in self.opts:
			if opt in ("-c", '--conffile'):
				self.config['configFile'] = forceFilename(arg)
			elif opt == "-v":
				print u"opsipxeconfd version %s" % __version__
				sys.exit(0)
		self.readConfigFile()
		self.setCommandlineConfig()

		if self.args[0] == u'version':
			print __version__
			sys.exit(0)

		elif self.args[0] == u'start':
			# Call signalHandler on signal SIGHUP, SIGTERM, SIGINT
			signal(SIGHUP, self.signalHandler)
			signal(SIGTERM, self.signalHandler)
			signal(SIGINT, self.signalHandler)

			if self.config['daemon']:
				logger.setConsoleLevel(LOG_NONE)
				self.daemonize()
			else:
				logger.setConsoleLevel(self.config['logLevel'])
				logger.setConsoleColor(True)

			self.createPidFile()
			try:
				# Start opsiconfd
				self._opsipxeconfd = Opsipxeconfd(self.config)
				self._opsipxeconfd.start()
				time.sleep(3)
				while self._opsipxeconfd.isRunning():
					time.sleep(1)
				self._opsipxeconfd.join(30)
			finally:
				self.removePidFile()

		else:
			con = ServerConnection(self.config['port'])
			result = con.sendCommand(u' '.join(forceUnicodeList(self.args)))
			if result:
				if result.startswith(u'(ERROR)'):
					print >> sys.stderr, result
					sys.exit(1)
				print >> sys.stdout, result
				sys.exit(0)
			else:
				sys.exit(1)

	def setDefaultConfig(self):
		self.config = {
			'pidFile': u'/var/run/opsipxeconfd/opsipxeconfd.pid',
			'configFile': u'/etc/opsi/opsipxeconfd.conf',
			'depotId': forceHostId(getfqdn(conf='/etc/opsi/global.conf')),
			'daemon': True,
			'logLevel': LOG_NOTICE,
			'logFile': u'/var/log/opsi/opsipxeconfd.log',
			'logFormat': u'[%l] [%D] %M (%F|%N)',
			'port': u'/var/run/opsipxeconfd/opsipxeconfd.socket',
			'pxeDir': u'/tftpboot/linux/pxelinux.cfg',
			'pxeConfTemplate': u'/tftpboot/linux/pxelinux.cfg/install',
			'uefiConfTemplate-x64': u'/tftpboot/linux/pxelinux.cfg/install-elilo-x64',
			'uefiConfTemplate-x86': u'/tftpboot/linux/pxelinux.cfg/install-elilo-x86',
			'maxConnections': 5,
			'maxPxeConfigWriters': 100,
			'backendConfigDir': u'/etc/opsi/backends',
			'dispatchConfigFile': u'/etc/opsi/backendManager/dispatch.conf',
		}

	def setCommandlineConfig(self):
		for (opt, arg) in self.opts:
			if opt in ("-F", "--no-fork"):
				self.config['daemon'] = False
			if opt in ("-l", "--loglevel"):
				self.config['logLevel'] = forceInt(arg)

	def createPidFile(self):
		logger.info(u"Creating pid file '%s'" % self.config['pidFile'])
		if os.path.exists(self.config['pidFile']):
			with open(self.config['pidFile'], 'r') as pf:
				oldPid = pf.readline().strip()

			if oldPid:
				running = False
				try:
					pids = execute("%s -x opsipxeconfd" % which("pidof"))[0].strip().split()
					for runningPid in pids:
						if runningPid == oldPid:
							running = True
							break
				except Exception as error:
					logger.error(error)

				if running:
					raise Exception(u"Another opsipxeconfd process is running (pid: %s), stop process first or change pidfile." % oldPid)

		pid = os.getpid()
		with open(self.config['pidFile'], "w") as pf:
			pf.write(str(pid))

	def removePidFile(self):
		try:
			if os.path.exists(self.config['pidFile']):
				logger.info(u"Removing pid file '%s'" % self.config['pidFile'])
				os.unlink(self.config['pidFile'])
		except Exception as error:
			logger.error(u"Failed to remove pid file '%s': %s" % (self.config['pidFile'], error))

	def signalHandler(self, signo, stackFrame):
		for thread in threading.enumerate():
			logger.debug(u"Running thread before signal: %s" % thread)

		if signo == SIGHUP:
			if self._opsipxeconfd:
				self.setDefaultConfig()
				self.readConfigFile()
				self.setCommandlineConfig()
				self._opsipxeconfd.setConfig(self.config)
				self._opsipxeconfd.reload()
		elif signo in (SIGTERM, SIGINT):
			if self._opsipxeconfd:
				self._opsipxeconfd.stop()

		for thread in threading.enumerate():
			logger.debug(u"Running thread after signal: %s" % thread)

	def readConfigFile(self):
		''' Get settings from config file '''
		logger.notice(u"Trying to read config from file: '%s'" % self.config['configFile'])

		try:
			configFile = ConfigFile(filename=self.config['configFile'])
			for line in configFile.parse():
				if line.count('=') == 0:
					logger.error(u"Parse error in config file: '%s', line '%s': '=' not found" % (self.config['configFile'], line))
					continue
				(option, value) = line.split(u'=', 1)
				option = option.strip()
				value = value.strip()
				if option == 'pid file':
					self.config['pidFile'] = forceFilename(value)
				elif option == 'log level':
					self.config['logLevel'] = forceInt(value)
				elif option == 'log file':
					self.config['logFile'] = forceFilename(value)
				elif option == 'log format':
					self.config['logFormat'] = forceUnicode(value)
				elif option == 'pxe config dir':
					self.config['pxeDir'] = forceFilename(value)
				elif option == 'pxe config template':
					self.config['pxeConfTemplate'] = forceFilename(value)
				elif option == 'max pxe config writers':
					self.config['maxPxeConfigWriters'] = forceInt(value)
				elif option == 'max control connections':
					self.config['maxConnections'] = forceInt(value)
				elif option == 'backend config dir':
					self.config['backendConfigDir'] = forceFilename(value)
				elif option == 'dispatch config file':
					self.config['dispatchConfigFile'] = forceFilename(value)
				else:
					logger.warning(u"Ignoring unknown option '%s' in config file: '%s'" % (option, self.config['configFile']))

		except Exception as error:
			# An error occured while trying to read the config file
			logger.error(u"Failed to read config file '%s': %s" % (self.config['configFile'], error))
			logger.logException(error)
			raise
		logger.notice(u"Config read")

	@staticmethod
	def usage(self):
		print u"\nUsage: %s [options] <command> [clientId] [args]..." % os.path.basename(sys.argv[0])
		print u"Commands:"
		print u"  version         Show version information and exit"
		print u"  start           Start main process"
		print u"  stop            Stop main process"
		print u"  status          Print status information of the main process"
		print u"  update          update PXE boot configuration for client"
		print u"Options:"
		print u"  -F, --no-fork   Do not fork to background"
		print u"  -c, --conffile  Location of config file"
		print u"  -l, --loglevel  Set log level (default: 5)"
		print u"        0=comment, 1=essential, 2=critical, 3=error, 4=warning, 5=notice, 6=info, 7=debug, 8=debug2, 9=confidential"
		print u""

	def daemonize(self):
		# Fork to allow the shell to return and to call setsid
		try:
			self._pid = os.fork()
			if self._pid > 0:
				# Parent exits
				sys.exit(0)
		except OSError as error:
			raise Exception(u"First fork failed: %e" % error)

		# Do not hinder umounts
		os.chdir("/")
		# Create a new session
		os.setsid()

		# Fork a second time to not remain session leader
		try:
			self._pid = os.fork()
			if self._pid > 0:
				sys.exit(0)
		except OSError as error:
			raise Exception(u"Second fork failed: %e" % error)

		logger.setConsoleLevel(LOG_NONE)

		# Close standard output and standard error.
		os.close(0)
		os.close(1)
		os.close(2)

		# Open standard input (0)
		if hasattr(os, "devnull"):
			os.open(os.devnull, os.O_RDWR)
		else:
			os.open("/dev/null", os.O_RDWR)

		# Duplicate standard input to standard output and standard error.
		os.dup2(0, 1)
		os.dup2(0, 2)
		sys.stdout = logger.getStdout()
		sys.stderr = logger.getStderr()


if __name__ == "__main__":
	logger.setConsoleLevel(LOG_WARNING)

	try:
		OpsipxeconfdInit()
	except SystemExit:
		pass
	except Exception as exception:
		logger.logException(exception)
		print >> sys.stderr, u"ERROR:", unicode(exception)
		sys.exit(1)

	sys.exit(0)
