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

# opsi-admin 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-admin - a commandline tool for accessing opsi.

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

from __future__ import print_function

import codecs
import fcntl
import gettext
import getopt
import json
import locale
import os
import os.path
import select
import subprocess
import sys
import time

from OPSI.Backend.BackendManager import BackendManager
from OPSI.Logger import (COLOR_CYAN, COLOR_RED, COLOR_GREEN, COLOR_YELLOW,
	COLOR_NORMAL, COLORS_AVAILABLE, COLOR_LIGHT_WHITE, COLOR_LIGHT_RED,
	LOG_NONE, LOG_ERROR, Logger)
from OPSI.System import which
from OPSI.Types import OpsiRpcError
from OPSI.Types import (forceBool, forceFilename, forceInt, forceUnicode,
	forceUnicodeLower)
from OPSI.Util import (blowfishDecrypt, deserialize, fromJson, getfqdn,
	objectToBeautifiedText, objectToBash, serialize, toJson)

import curses

__version__ = '4.0.6.10'

backend = None
exitZero = False
shell = None
logFile = None
logLevel = LOG_NONE
logger = Logger()
interactive = False
logger.setLogFormat(u"[%l] %M (%F|%N)")

outEncoding = sys.stdout.encoding
inEncoding = sys.stdin.encoding
if not outEncoding or (outEncoding == 'ascii'):
	outEncoding = locale.getpreferredencoding()
if not outEncoding or (outEncoding == 'ascii'):
	outEncoding = 'utf-8'

if not inEncoding or (inEncoding == 'ascii'):
	inEncoding = outEncoding

UNCOLORED_LOGO = """\
                                   .:.:::::.
                                   ;      ::
                                   ;      ;.
                                   -:....::
                                     . --
                                  ;:........
                                     -----  -
                                      ..
                                     .||.
                                  .._|||=_..
                               _=||++~~-~++||=,
                            _=|>~-           ~+|;.
                          .=|+-  _; ____=___.   +|;
                         .||-. .=i`++++++++||=   -|=.
           . ....        ||`. ..|>         =|+    -|=        . ....
          = -----:.     =|; ...:|= . .   . ||;     =|;      ; -- --::
         .:      ;.   ._||`.. . || . . .  .|+`     .||_,    =      :.
          ;.    ::  -<||+|.. ...:|;__...._=|=  . . .||+|+-  ;.    .;
          --::::-      -+|;.. . .-+||||||||+ .  .  :|;-      --:;::
        ..              -|+ ... ...  --- .  . .. ..||     .. .
         -:::::;:: .     =|=.._=;___:...:.:.____. =|`      --:;:;;::..
                          ~||,-~+||||||||||||>~ _||`
                           -=|=_...---~~-~--  _=i:
                             -~||=__:.-..:__|||~ .
                                -~+++||||++~--
          opsi-admin {version}
""".format(version=__version__).split('\n')

LOGO = [{"color": COLOR_CYAN, "text": line} for line in UNCOLORED_LOGO]

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

	def _(string):
		return string


class ErrorInResultException(Exception):
	"Indicates that there is an error in the result."


def signalHandler(signo, stackFrame):
	logger.info(u"Received signal %s" % signo)
	if signo == SIGINT:
		if shell:
			shell.sigint()
	elif signo == SIGQUIT:
		sys.exit(0)


def usage():
	print((u"\n" + _(u"Usage: %s [options] [command] [args...]") % os.path.basename(sys.argv[0])).encode(outEncoding))
	print((_(u"Options:")).encode(outEncoding))
	print((u"  -h, --help           " + _(u"Display this text")).encode(outEncoding))
	print((u"  -V, --version        " + _(u"Show version and exit")).encode(outEncoding))
	print((u"  -u, --username       " + _(u"Username (default: current user)")).encode(outEncoding))
	print((u"  -p, --password       " + _(u"Password (default: prompt for password)")).encode(outEncoding))
	print((u"  -a, --address        " + _(u"URL of opsiconfd (default: https://localhost:4447/rpc)")).encode(outEncoding))
	print((u"  -d, --direct         " + _(u"Do not use opsiconfd")).encode(outEncoding))
	print((u"      --no-depot       " + _(u"Do not use depotserver backend")).encode(outEncoding))
	print((u"  -l, --loglevel       " + _(u"Set log level (default: 3)")).encode(outEncoding))
	print((u"                       " + _(u"0=nothing, 1=essential, 2=critical, 3=error, 4=warning")).encode(outEncoding))
	print((u"                       " + _(u"5=notice, 6=info, 7=debug, 8=debug2, 9=confidential")).encode(outEncoding))
	print((u"  -f, --log-file       " + _(u"Path to log file")).encode(outEncoding))
	print((u"  -i, --interactive    " + _(u"Start in interactive mode")).encode(outEncoding))
	print((u"  -c, --colorize       " + _(u"Colorize output")).encode(outEncoding))
	print((u"  -S, --simple-output  " + _(u"Simple output (only for scalars, lists)")).encode(outEncoding))
	print((u"  -s, --shell-output   " + _(u"Shell output")).encode(outEncoding))
	print((u"  -r, --raw-output     " + _(u"Raw output")).encode(outEncoding))
	print((u"  --exit-zero          " + _(u"Always exit with exit code 0.")).encode(outEncoding))
	print((u"").encode(outEncoding))


def main(argv):
	os.umask(077)
	global interactive
	global exitZero

	logLevel = LOG_NONE
	try:
		import pwd
		username = forceUnicode(pwd.getpwuid(os.getuid())[0])
	except Exception:
		username = u''

	password = u''
	address = u'https://localhost:4447/rpc'
	output = u'JSON'
	color = False
	direct = False
	depotBackend = True

	try:
		(opts, args) = getopt.getopt(argv, "u:p:a:l:f:dicsSrhV",
			[
				"username=", "password=", "address=", "loglevel=", "log-file=",
				"interactive", "direct", "no-depot", "colorize",
				"shell-output", "simple-output", "raw-output", "help",
				"version", "exit-zero"
			]
		)
	except getopt.GetoptError:
		usage()
		sys.exit(1)

	for (opt, arg) in opts:
		if opt in ("-h", "--help"):
			usage()
			sys.exit(0)
		elif opt in ("-V", "--version"):
			print("%s %s" % (os.path.basename(sys.argv[0]), __version__))
			sys.exit(0)
		elif opt in ("-u", "--username"):
			username = forceUnicode(arg)
		elif opt in ("-p", "--password"):
			password = forceUnicode(arg)
		elif opt in ("-l", "--loglevel"):
			logLevel = forceInt(arg)
		elif opt in ("-f", "--log-file"):
			global logFile
			logFile = forceFilename(arg)
		elif opt in ("-a", "--address"):
			address = forceUnicode(arg)
		elif opt in ("-d", "--direct"):
			direct = True
		elif opt in ("--no-depot",):
			depotBackend = False
		elif opt in ("-i", "--interactive"):
			interactive = True
		elif opt in ("-c", "--colorize"):
			color = True
		elif opt in ("-s", "--shell-output"):
			output = u'SHELL'
		elif opt in ("-S", "--simple-output"):
			output = u'SIMPLE'
		elif opt in ("-r", "--raw-output"):
			output = u'RAW'
		elif opt == '--exit-zero':
			exitZero = True

	if logFile:
		startLogFile(logFile)

	logger.setConsoleLevel(logLevel)
	logger.setColor(color)

	if interactive:
		logger.setConsoleLevel(LOG_NONE)

	global backend
	try:
		if direct:
			# Create BackendManager
			backend = BackendManager(
				dispatchConfigFile=u'/etc/opsi/backendManager/dispatch.conf',
				backendConfigDir=u'/etc/opsi/backends',
				extensionConfigDir=u'/etc/opsi/backendManager/extend.d',
				depotBackend=depotBackend,
				hostControlBackend=True,
				hostControlSafeBackend=True
			)

		else:
			# Connect to opsiconfd
			if not password:
				try:
					import getpass
					password = unicode(getpass.getpass(), inEncoding)
				except Exception:
					pass

			opsiadminUserDir = forceFilename(os.path.join(os.environ.get('HOME'), u'.opsi-admin'))
			if not os.path.exists(opsiadminUserDir):
				try:
					os.mkdir(opsiadminUserDir)
				except OSError as error:
					logger.info("Could not create {0}.".format(opsiadminUserDir))

			sessionId = None
			try:
				with codecs.open(os.path.join(opsiadminUserDir, u'session'), 'r', 'utf-8') as session:
					for line in session:
						line = line.strip()
						if line:
							sessionId = forceUnicode(line)
							break
			except Exception as error:
				logger.error(u"Failed to read session file '%s': %s" % (os.path.join(opsiadminUserDir, 'session'), forceUnicode(error)))

			from OPSI.Backend.JSONRPC import JSONRPCBackend
			backend = JSONRPCBackend(
				address=address,
				username=username,
				password=password,
				application='opsi-admin version %s' % __version__,
				sessionId=sessionId
			)
			logger.info(u'Connected')

			sessionId = backend.jsonrpc_getSessionId()
			if sessionId:
				try:
					with codecs.open(os.path.join(opsiadminUserDir, u'session'), 'w', 'utf-8') as session:
						session.write(u"%s\n" % sessionId)
				except Exception as error:
					logger.error(u"Failed to write session file '%s': %s" % (os.path.join(opsiadminUserDir, u'session'), forceUnicode(error)))

		cmdline = u''
		if len(args) > 0:
			for i in range(len(args)):
				args[i] = unicode(args[i], inEncoding)
				logger.info(u"arg[%d]: %s" % (i, args[i]))
				if i == 0:
					cmdline = args[i]
				elif (args[i].find(u' ') != -1) or (len(args[i]) == 0):
					cmdline += u" '%s'" % args[i]
				else:
					cmdline += u" %s" % args[i]

		(readSelection, _unused, _unused) = select.select([sys.stdin], [], [], 0.2)
		if sys.stdin in readSelection:
			read = unicode(sys.stdin.read(), inEncoding, 'replace')
			read = read.replace('\r', '').replace('\n', '')
			if read:
				cmdline += u" '%s'" % read

		logger.debug(u"cmdline: '%s'" % cmdline)

		global shell
		if interactive:
			try:
				logger.notice(u"Starting interactive mode")
				shell = Shell(prompt=u'%s@opsi-admin>' % username, output=output, color=color, cmdline=cmdline)
				shell.setInfoline(u"Connected to %s" % address)

				for line in LOGO:
					shell.appendLine(line.get('text'), line.get('color'))
				shell.run()
			except Exception as error:
				logger.logException(forceUnicode(error))
				raise
		elif cmdline:
			def searchForError(obj):
				if isinstance(obj, dict):
					try:
						if obj['error']:
							raise ErrorInResultException(obj['error'])
					except KeyError:
						for key in obj:
							searchForError(obj[key])
				elif isinstance(obj, list):
					for element in obj:
						searchForError(element)

			try:
				shell = Shell(prompt=u'%s@opsi-admin>' % username, output=output, color=color)
				for cmd in cmdline.split(u'\n'):
					if cmd:
						shell.cmdline = cmd
						shell.execute()

				logger.debug(shell.getLines())
				for line in shell.lines:
					print(forceUnicode(line['text']).rstrip().encode(outEncoding), file=sys.stdout)

				try:
					resultAsJSON = json.loads(u'\n'.join([line['text'] for line in shell.lines]))
					searchForError(dict(resultAsJSON))
				except (TypeError, ValueError) as error:
					logger.debug(u"Conversion to dict failed: {0}".format(error))
			except Exception as error:
				logger.logException(forceUnicode(error))
				raise error
		else:
			usage()
			sys.exit(1)
	finally:
		if backend:
			try:
				backend.backend_exit()
			except Exception:
				pass


def startLogFile(logFile):
	with codecs.open(logFile, 'w', 'utf-8') as log:
		log.write(u"Starting log at: %s" % forceUnicode(time.strftime(u"%a, %d %b %Y %H:%M:%S")))

	logger.setLogFile(logFile)
	logger.setFileLevel(logLevel)


class Shell:

	def __init__(self, prompt=u'opsi-admin>', output=u'JSON', color=True, cmdline=u''):
		self.color = forceBool(color)
		self.output = forceUnicode(output)
		self.running = False
		self.screen = None
		self.cmdBufferSize = 1024
		self.userConfigDir = None
		self.prompt = forceUnicode(prompt)
		self.infoline = u'opsi admin started'
		self.yMax = 0
		self.xMax = 0
		self.pos = len(cmdline)
		self.lines = []
		self.linesBack = 0
		self.linesMax = 0
		self.paramPos = -1
		self.currentParam = None
		self.cmdListPos = 0
		self.cmdList = []
		self.cmdline = forceUnicode(cmdline)
		self.shellCommand = u''
		self.reverseSearch = None
		self.commands = [
			CommandMethod(),
			CommandSet(),
			CommandHelp(),
			CommandQuit(),
			CommandExit(),
			CommandHistory(),
			CommandLog(),
			CommandTask()
		]

		home = os.environ.get('HOME')
		if not home:
			logger.debug('Environment has no $HOME set.')
			home = os.path.expanduser('~')

		if home:
			self.userConfigDir = forceFilename(os.path.join(home, '.opsi-admin'))
			if not os.path.isdir(self.userConfigDir):
				try:
					os.mkdir(self.userConfigDir)
				except OSError as error:
					logger.error(u"Failed to create user dir {0!r}: {1}".format(self.userConfigDir, forceUnicode(error)))
		else:
			logger.error(u'Failed to get home directory from environment!')

		historyFile = forceFilename(os.path.join(self.userConfigDir, u'history'))
		try:
			with codecs.open(historyFile, 'r', 'utf-8', 'replace') as history:
				for line in history:
					if not line:
						continue
					self.cmdList.append(line.strip())
					self.cmdListPos += 1
		except Exception as error:
			logger.error(u"Failed to read history file {0!r}: {1}".format(historyFile, forceUnicode(error)))

	def setColor(self, color):
		color = forceBool(color)
		if color != self.color:
			self.color = color
			self.initScreen()

	def getLines(self):
		return self.lines

	def initScreen(self):
		if not self.screen:
			self.screen = curses.initscr()
		curses.noecho()
		curses.cbreak()
		self.screen.keypad(1)
		self.screen.clear()

		self.yMax, self.xMax = self.screen.getmaxyx()
		self.linesMax = self.yMax - 2

		if self.color:
			curses.start_color()

			curses.init_pair(1, curses.COLOR_GREEN, curses.COLOR_BLACK)
			curses.init_pair(2, curses.COLOR_CYAN, curses.COLOR_BLACK)
			curses.init_pair(3, curses.COLOR_YELLOW, curses.COLOR_BLACK)
			curses.init_pair(4, curses.COLOR_MAGENTA, curses.COLOR_BLACK)
			curses.init_pair(5, curses.COLOR_RED, curses.COLOR_BLACK)

	def exitScreen(self):
		if self.screen:
			curses.nocbreak()
			self.screen.keypad(0)
			curses.echo()
			curses.endwin()

	def sigint(self):
		self.pos = 0
		self.setCmdline(u'')
		self.reverseSearch = None

	def run(self):
		self.running = True

		self.initScreen()

		if self.cmdline:
			for cmd in self.cmdline.split(u'\n'):
				self.cmdline = cmd
				self.appendLine(u"%s %s" % (self.prompt, self.cmdline))
				if self.cmdline:
					try:
						self.execute()
					except Exception as error:
						lines = forceUnicode(error).split(u'\n')
						lines[0] = u"ERROR: %s" % lines[0]
						for line in lines:
							self.appendLine(line, COLOR_RED)

				if len(self.lines) > self.yMax - 2:
					self.linesBack = 0
		else:
			self.display()

		while self.running:
			self.getCommand()

	def exit(self):
		if interactive and logFile and os.path.exists(logFile):
			if self.question(_("Delete log-file '%s'?") % logFile):
				try:
					os.unlink(logFile)
				except OSError as error:
					logger.error(u"Failed to delete log-file {0!r}: {1}".format(logFile, forceUnicode(error)))

		historyFilePath = os.path.join(self.userConfigDir, 'history')
		try:
			with codecs.open(historyFilePath, 'w', 'utf-8') as history:
				for line in self.cmdList:
					if not line or line in ('quit', 'exit'):
						continue
					history.write(u"%s\n" % line)
		except Exception as error:
			logger.error(u"Failed to write history file {0!r}: {1}".format(historyFilePath, forceUnicode(error)))

		self.exitScreen()
		self.running = False

	def bell(self):
		sys.stderr.write('\a')

	def display(self):
		if not self.screen:
			return
		self.screen.move(0, 0)
		self.screen.clrtoeol()
		shellLine = self.infoline + (self.xMax - len(self.infoline)) * u' '
		try:
			self.screen.addstr(shellLine.encode(outEncoding), curses.A_REVERSE)
		except Exception as error:
			logger.error(u"Failed to add string {0!r}: {1}".format(shellLine, forceUnicode(error)))

		height = int(len(self.prompt + u' ' + self.cmdline) / self.xMax) + 1
		clear = self.xMax - (len(self.prompt + u' ' + self.cmdline) % self.xMax) - 1

		self.linesMax = self.yMax - height - 1
		self.screen.move(self.yMax - height, 0)
		self.screen.clrtoeol()
		shellLine = self.prompt + u' ' + self.cmdline + u' ' * clear
		try:
			self.screen.addstr(shellLine.encode(outEncoding), curses.A_BOLD)
		except Exception as error:
			logger.error(u"Failed to add string {0!r}: {1}".format(shellLine, forceUnicode(error)))

		for i in range(0, self.linesMax):
			self.screen.move(self.linesMax - i, 0)
			self.screen.clrtoeol()
			shellLine = u''
			color = None
			if len(self.lines) - self.linesBack > i:
				shellLine = self.lines[len(self.lines) - self.linesBack - 1 - i]['text']
				if self.color:
					color = self.lines[len(self.lines) - self.linesBack - 1 - i]['color']

			if color:
				if color == COLOR_NORMAL:
					color = curses.A_BOLD
				elif color == COLOR_GREEN:
					color = curses.color_pair(1)
				elif color == COLOR_CYAN:
					color = curses.color_pair(2)
				elif color == COLOR_LIGHT_WHITE:
					color = curses.A_BOLD
				elif color == COLOR_YELLOW:
					color = curses.color_pair(3)
				elif color == COLOR_LIGHT_RED:
					color = curses.color_pair(4)
				elif color == COLOR_RED:
					color = curses.color_pair(5)

			try:
				if color is not None:
					self.screen.addstr(shellLine.encode(outEncoding), color)
				else:
					self.screen.addstr(shellLine.encode(outEncoding))
			except Exception as error:
				logger.error(u"Failed to add string {0!r}: {1}".format(shellLine, forceUnicode(error)))

		moveY = self.yMax - height + int((len(self.prompt + u' ') + self.pos) / self.xMax)
		moveX = ((len(self.prompt + u' ') + self.pos) % self.xMax)
		self.screen.move(moveY, moveX)
		self.screen.refresh()

	def appendLine(self, line, color=None, refresh=True):
		line = forceUnicode(line)

		if not color:
			if line.startswith(COLOR_NORMAL):
				color = COLOR_NORMAL
			elif line.startswith(COLOR_GREEN):
				color = COLOR_GREEN
			elif line.startswith(COLOR_CYAN):
				color = COLOR_CYAN
			elif line.startswith(COLOR_LIGHT_WHITE):
				color = COLOR_LIGHT_WHITE
			elif line.startswith(COLOR_YELLOW):
				color = COLOR_YELLOW
			elif line.startswith(COLOR_LIGHT_RED):
				color = COLOR_LIGHT_RED
			elif line.startswith(COLOR_RED):
				color = COLOR_RED

		for availableColor in COLORS_AVAILABLE:
			line = line.replace(availableColor, u'')

		while self.xMax and (len(line) > self.xMax):
			self.lines.append({"text": line[:self.xMax], "color": color})
			line = line[self.xMax:]

		self.lines.append({"text": line, "color": color})
		if refresh:
			self.display()

	def setCmdline(self, cmdline, refresh=True):
		self.cmdline = forceUnicode(cmdline)
		if refresh:
			self.display()

	def setInfoline(self, infoline, refresh=True):
		self.infoline = forceUnicode(infoline)
		if refresh:
			self.display()

	def getParams(self):
		self.parseCmdline()
		return self.params

	def getParam(self, i):
		self.parseCmdline()
		if len(self.params) > i:
			return self.params[i]
		return u''

	def parseCmdline(self):
		self.params = []
		self.currentParam = None
		self.paramPos = -1

		if not self.cmdline:
			return

		self.shellCommand = u''
		cmdline = self.cmdline
		if '|' in cmdline:
			split = -1
			qCount = 0
			dqCount = 0
			parts = cmdline.split(u'|')
			for i in range(len(parts)):
				qCount += parts[i].count(u"'")
				dqCount += parts[i].count(u'"')
				if (qCount % 2 == 0) and (dqCount % 2 == 0):
					split = i
					break
			if split >= 0:
				cmdline = u'|'.join(parts[:split + 1])
				self.shellCommand = u'|'.join(parts[split + 1:]).lstrip()

		cur = 0
		quote = None

		for i in range(len(cmdline)):
			logger.debug2(u"parseCmdline(): char '%s', quote: %s, cur: %d, params: %s" % (cmdline[i], quote, cur, self.params))
			if len(self.params) < cur + 1:
				self.params.append(u'')

			if i == self.pos - 1:
				self.paramPos = cur

			if cmdline[i] == u"'":
				if quote is None:
					quote = u"'"
				elif quote == u"'":
					if not self.params[cur]:
						cur += 1
					quote = None
				else:
					self.params[cur] += u'\''

			elif cmdline[i] == u'"':
				if quote is None:
					self.params[cur] += u'"'
					quote = u'"'
				elif quote == u'"':
					self.params[cur] += u'"'
					if not self.params[cur]:
						cur += 1
					quote = None
				else:
					self.params[cur] += u'"'

			elif cmdline[i] == u" ":
				if quote is not None:
					self.params[cur] += cmdline[i]
				elif len(self.params[cur]) > 0:
					cur += 1

			else:
				self.params[cur] += cmdline[i]

		if not quote and self.params and self.params[-1] and self.pos == len(cmdline) and cmdline.endswith(u' '):
			self.params.append(u'')
			self.paramPos += 1

		if self.params:
			self.currentParam = self.params[self.paramPos]
		else:
			self.currentParam = u''

		logger.debug(u"cmdline: %s" % cmdline)
		logger.debug2(u"paramPos: %s, currentParam: %s, params: %s" \
				% (self.paramPos, self.currentParam, self.params))
		if self.paramPos >= len(self.params):
			logger.error(
				u"Assertion 'self.paramPos < len(self.params)' failed: "
				u"self.paramPos: %s, len(self.params): %s" % (
					self.paramPos,
					len(self.params)
				)
			)
			self.paramPos = len(self.params) - 1

	def execute(self):
		logger.info(u"Execute: {0!r}".format(self.cmdline))
		self.cmdList.append(self.cmdline)
		if len(self.cmdList) > self.cmdBufferSize:
			del self.cmdList[0]
		self.cmdListPos = len(self.cmdList)
		invalid = True
		for command in self.commands:
			if command.getName() == self.getParam(0):
				invalid = False
				try:
					command.execute(self, self.getParams()[1:])
				except Exception as error:
					logger.logException(error)
					message = u"Failed to execute {0!r}: {1}".format(self.cmdline, forceUnicode(error))
					logger.error(message)
					raise Exception(message)
				break

		if invalid:
			raise ValueError(_(u"Invalid command: '%s'") % self.getParam(0))

	def question(self, question):
		question = forceUnicode(question)
		if interactive:
			self.screen.move(self.yMax - 1, 0)
			self.screen.clrtoeol()
			self.screen.addstr((question + u' (n/y)').encode(inEncoding))
			self.screen.refresh()
			char = None
			while True:
				char = self.screen.getch()
				if char and char >= 0 and char < 256 and char != 10:
					if chr(char) == 'y':
						return True
					elif chr(char) == 'n':
						return False
		return False

	def getPassword(self):
		password1 = u''
		password2 = u''
		while not password1 or (password1 != password2):
			if interactive:
				self.screen.move(self.yMax - 1, 0)
				self.screen.clrtoeol()
				self.screen.addstr(_(u"Please type password:").encode(outEncoding))
				self.screen.refresh()
				password1 = unicode(self.screen.getstr(), inEncoding)

				self.screen.move(self.yMax - 1, 0)
				self.screen.clrtoeol()
				self.screen.addstr(_(u"Please retype password:").encode(outEncoding))
				self.screen.refresh()
				password2 = unicode(self.screen.getstr(), inEncoding)

				if password1 != password2:
					self.screen.move(self.yMax - 1, 0)
					self.screen.clrtoeol()
					self.screen.addstr(_(u"Supplied passwords do not match").encode(outEncoding))
					self.screen.refresh()
					time.sleep(2)
			else:
				import getpass
				password1 = password2 = unicode(getpass.getpass(), inEncoding)

		logger.confidential(u"Got password '%s'" % password1)
		return password1

	def getCommand(self):
		char = None
		self.pos = 0
		self.setCmdline(u'')
		self.reverseSearch = None

		while not char or (char != 10):
			char = self.screen.getch()
			textInput = False

			if not char or char < 0:
				continue

			if char == curses.KEY_RESIZE:
				# window resized
				self.yMax, self.xMax = self.screen.getmaxyx()
				self.display()
				continue

			elif char == curses.KEY_UP:
				if len(self.cmdList) > 0 and self.cmdListPos > 0:
					self.cmdListPos -= 1
					self.pos = len(self.cmdList[self.cmdListPos])
					self.setCmdline(self.cmdList[self.cmdListPos])

			elif char == curses.KEY_DOWN:
				if len(self.cmdList) > 0 and self.cmdListPos < len(self.cmdList):
					self.cmdListPos += 1
					if self.cmdListPos == len(self.cmdList):
						self.pos = 0
						self.setCmdline('')
					else:
						self.pos = len(self.cmdList[self.cmdListPos])
						self.setCmdline(self.cmdList[self.cmdListPos])

			elif char == curses.KEY_LEFT:
				if self.pos > 0:
					self.pos -= 1
					self.setCmdline(self.cmdline)

			elif char == curses.KEY_RIGHT:
				if self.pos < len(self.cmdline):
					self.pos += 1
					self.setCmdline(self.cmdline)

			elif char == curses.KEY_HOME:
				self.pos = 0
				self.setCmdline(self.cmdline)

			elif char == curses.KEY_END:
				self.pos = len(self.cmdline)
				self.setCmdline(self.cmdline)

			elif char == curses.KEY_NPAGE:
				if len(self.lines) > self.yMax - 2:
					self.linesBack -= 5
					if self.linesBack < 0:
						self.linesBack = 0

					self.display()

			elif char == curses.KEY_PPAGE:
				if len(self.lines) > self.yMax - 2:
					self.linesBack += 5
					if self.linesBack > len(self.lines) - self.yMax + 2:
						self.linesBack = len(self.lines) - self.yMax + 2

					self.display()

			elif char == 4:
				# ^D
				self.exit()

			elif char == 10:
				# Enter
				self.pos = len(self.cmdline)
				self.setCmdline(self.cmdline)

			elif char == 18:
				# ^R
				if self.reverseSearch is None:
					self.setInfoline("reverse-i-search")
					self.reverseSearch = ''
				else:
					self.setInfoline("")
					self.reverseSearch = None
				continue

			elif char == 9:
				# tab 		|<- ->|
				# Auto-completion
				completions = []

				params = self.getParams()
				if self.paramPos >= 0:
					params[self.paramPos] = self.currentParam

				for command in self.commands:
					if self.paramPos < 0:
						completions.append(command.getName())

					elif params[0] == command.getName():
						if self.paramPos >= 1:
							completions = command.completion(params[1:], self.paramPos)
						else:
							completions = [command.getName()]
						break

					elif command.getName().startswith(params[0]):
						completions.append(command.getName())

				if len(completions) == 1:
					self.setCmdline(self.cmdline[:self.pos] + \
							completions[0][len(params[self.paramPos]):] + \
							self.cmdline[self.pos:])
					self.pos += len(completions[0][len(params[self.paramPos]):])

					if self.pos == len(self.cmdline):
						self.cmdline += ' '
						self.pos += 1

					self.setCmdline(self.cmdline)

				elif len(completions) > 1:
					match = completions[0]
					lines = []
					longest = 0
					for comp in completions:
						for c in range(len(comp)):
							if c > len(match) - 1:
								break
							elif comp[c] != match[c]:
								match = match[:c]
								break
						if len(comp) > longest:
							longest = len(comp)

					curLine = ''
					i = 0
					while i < len(completions):
						while (i < len(completions)) and (not curLine or (len(curLine) + longest < self.xMax - 5)):
							pf = '%s %-' + str(longest) + 's'
							curLine = pf % (curLine, completions[i])
							i += 1
						lines.append({"text": curLine, "color": None})
						curLine = ''

					if self.paramPos < 0:
						self.currentParam = ""

					self.lines.append(
						{
							"text": self.prompt + ' ' + \
									self.cmdline[:self.pos - len(self.currentParam)] + \
									match.strip() + \
									self.cmdline[self.pos:],
							"color": None
						}
					)

					self.lines.extend(lines)

					self.setCmdline(self.cmdline[:self.pos - len(self.currentParam)] + match.strip() + self.cmdline[self.pos:])

					self.pos += len(match) - len(self.currentParam)

					self.setCmdline(self.cmdline)
				else:
					self.bell()

			else:
				textInput = True
				newPos = self.pos
				newCmdline = self.cmdline

				if char == 263:
					# backspace	<--
					if self.reverseSearch is not None:
						self.reverseSearch = self.reverseSearch[:-1]
						self.setInfoline('reverse-i-search: %s' % self.reverseSearch)
					elif self.pos > 0:
						newPos = self.pos - 1
						newCmdline = self.cmdline[:newPos] + self.cmdline[self.pos:]
				elif char == 330:
					# del
					if self.reverseSearch is not None:
						pass
					elif len(self.cmdline) > 0:
						newCmdline = self.cmdline[:self.pos] + self.cmdline[self.pos + 1:]

				else:
					try:
						curses.ungetch(char)
						char = self.screen.getkey()

						try:
							char = unicode(char, inEncoding)
						except Exception:
							char += self.screen.getkey()
							char = unicode(char, inEncoding)

						if self.reverseSearch is not None:
							self.reverseSearch += char
						else:
							newPos = self.pos + 1
							newCmdline = self.cmdline[0:self.pos] + char + self.cmdline[self.pos:]
					except Exception as error:
						logger.error("Failed to add char '%s': %s" % (char, forceUnicode(error)))
				try:
					if self.reverseSearch is not None:
						self.setInfoline('reverse-i-search: %s' % self.reverseSearch)
						found = False
						for i in range(len(self.cmdList) - 1, -1, -1):
							if self.reverseSearch in self.cmdList[i]:
								found = True
								newCmdline = self.cmdList[i]
								break

						if not found and char not in (263, 330):
							self.bell()
						newPos = len(newCmdline)

					self.pos = newPos
					self.setCmdline(newCmdline)
				except Exception as error:
					self.setInfoline(forceUnicode(error))
					pass

			if not textInput:
				if self.reverseSearch is not None:
					self.reverseSearch = None
					self.setInfoline("")

		self.cmdline = self.cmdline.strip()

		self.appendLine(self.prompt + ' ' + self.cmdline)
		if self.cmdline:
			try:
				self.execute()
			except Exception as error:
				lines = forceUnicode(error).split('\n')
				lines[0] = "ERROR: %s" % lines[0]
				for line in lines:
					self.appendLine(line, COLOR_RED)

		if len(self.lines) > self.yMax - 2:
			self.linesBack = 0


class Command:
	def __init__(self, name):
		self.name = forceUnicode(name)

	def getName(self):
		return self.name

	def getDescription(self):
		return u""

	def completion(self, params, paramPos):
		return []

	def help(self, shell):
		shell.appendLine(u"")

	def execute(self, shell, params):
		raise NotImplementedError(u"Nothing to do.")


class CommandMethod(Command):
	def __init__(self):
		Command.__init__(self, u'method')
		self.interface = backend.backend_getInterface()

	def getDescription(self):
		return _(u"Execute a config-interface-method")

	def help(self, shell):
		shell.appendLine(u"\r{0}\n".format(_(u"Methods are:")))
		for method in backend.backend_getInterface():
			logger.debug(method)
			shell.appendLine(u"\r%s\n" % method.get('name'))

	def completion(self, params, paramPos):
		completions = []

		if paramPos == 0:
			completions.append(u'list')
			for m in self.interface:
				completions.append(m.get(u'name'))

		elif paramPos == 1:
			if u'list'.startswith(params[0]):
				completions.append(u'list')
			for m in self.interface:
				if m.get('name').startswith(params[0]):
					completions.append(m.get('name'))

		elif paramPos >= 2:
			for m in self.interface:
				if m.get('name') == params[0]:
					if len(m.get('params')) >= len(params) - 1:
						completions = [m.get('params')[paramPos - 2]]
					break

		return completions

	def execute(self, shell, params):
		if len(params) <= 0:
			shell.appendLine(_(u'No method defined'))
			return

		method = params[0]

		if method == u'list':
			for m in self.interface:
				shell.appendLine(u"%s%s" % (m.get('name'), tuple(m.get('params'))), refresh=False)
			shell.display()
			return

		methodInterface = None
		for m in self.interface:
			if method == m['name']:
				methodInterface = m
				break

		if not methodInterface:
			raise OpsiRpcError(u"Method '%s' is not valid" % method)

		params = params[1:]
		keywords = {}
		if methodInterface['keywords']:
			l = 0
			if methodInterface['args']:
				l += len(methodInterface['args'])
			if methodInterface['varargs']:
				l += len(methodInterface['varargs'])
			if len(params) >= l:
				# Do not create Object instances!
				params[-1] = fromJson(params[-1], preventObjectCreation=True)
				if not isinstance(params[-1], dict):
					raise Exception(u"kwargs param is not a dict: %s" % params[-1])

				for (key, value) in params.pop(-1).items():
					keywords[str(key)] = deserialize(value)

		def createObjectOrString(obj):
			"Tries to return object from JSON. If this fails returns unicode."
			try:
				return fromJson(obj)
			except Exception as error:
				logger.debug(u"Not a json string {0!r}: {1}".format(obj, forceUnicode(error)))
				return forceUnicode(obj)

		params = [createObjectOrString(item) for item in params]

		pString = unicode(params)[1:-1]
		if keywords:
			pString += u', ' + unicode(keywords)
		if len(pString) > 200:
			pString = pString[:200] + u'...'

		result = None

		logger.info(u"Executing:  %s(%s)" % (method, pString))
		shell.setInfoline(u"Executing:  %s(%s)" % (method, pString))
		start = time.time()

		if keywords:
			result = eval("backend.%s(*params, **keywords)" % method)
		else:
			result = eval("backend.%s(*params)" % method)

		duration = time.time() - start
		logger.debug(u'Took %0.3f seconds to process: %s(%s)' % (duration, method, pString))
		shell.setInfoline(_(u'Took %0.3f seconds to process: %s(%s)') % (duration, method, pString))
		result = serialize(result)
		logger.debug2(result)

		if result:
			lines = []
			if shell.output == u'RAW':
				lines.append(toJson(result))

			elif shell.output == u'JSON':
				lines = objectToBeautifiedText(result).split(u'\n')

			elif shell.output == u'SHELL':
				bashVars = objectToBash(result, {})
				for index in range(len(bashVars) - 1, -2, -1):
					if index == -1:
						index = ''

					value = bashVars.get('RESULT%s' % index)
					if value:
						lines.append(u'RESULT%s=%s' % (index, value))

			elif shell.output == u'SIMPLE':
				if isinstance(result, dict):
					for (key, value) in result.items():
						if isinstance(value, bool):
							value = forceUnicodeLower(value)
						lines.append(u'%s=%s' % (key, value))
				elif isinstance(result, (tuple, list, set)):
					for resultElement in result:
						if isinstance(resultElement, dict):
							for (key, value) in resultElement.items():
								if isinstance(value, bool):
									value = forceUnicodeLower(value)
								lines.append(u'%s=%s' % (key, value))
							lines.append(u'')
						elif isinstance(resultElement, (tuple, list)):
							raise Exception(u"Simple output not possible for list of lists")
						else:
							lines.append(forceUnicode(resultElement))
				else:
					lines.append(forceUnicode(result))
			else:
				lines.append(forceUnicode(result))

			if shell.shellCommand:
				logger.notice(u"Executing: '%s'" % shell.shellCommand)

				proc = subprocess.Popen(
					shell.shellCommand,
					shell=True,
					stdin=subprocess.PIPE,
					stdout=subprocess.PIPE,
					stderr=subprocess.PIPE,
				)

				flags = fcntl.fcntl(proc.stdout, fcntl.F_GETFL)
				fcntl.fcntl(proc.stdout, fcntl.F_SETFL, flags | os.O_NONBLOCK)

				flags = fcntl.fcntl(proc.stderr, fcntl.F_GETFL)
				fcntl.fcntl(proc.stderr, fcntl.F_SETFL, flags | os.O_NONBLOCK)

				encoding = proc.stdout.encoding
				if not encoding:
					encoding = inEncoding
				exitCode = None

				buf = ''
				err = ''

				while exitCode is None:
					exitCode = proc.poll()
					if lines:
						for line in lines:
							proc.stdin.write((u"%s\n" % line).encode(outEncoding, 'replace'))
						lines = []
						proc.stdin.close()

					try:
						string = proc.stdout.read()
						if len(string) > 0:
							buf += string
					except IOError as error:
						if error.errno != 11:
							raise

					try:
						string = proc.stderr.read()
						if len(string) > 0:
							err += string
					except IOError as error:
						if error.errno != 11:
							raise

				if exitCode != 0:
					raise Exception(u'Exitcode: %s\n%s' % ((exitCode * -1), unicode(err, encoding, 'replace')))

				lines = unicode(buf, encoding, 'replace').split(u'\n')

			for line in lines:
				shell.appendLine(line, COLOR_GREEN)


class CommandSet(Command):
	def __init__(self):
		Command.__init__(self, u'set')

	def getDescription(self):
		return _(u"Settings")

	def completion(self, params, paramPos):
		completions = []

		if paramPos == 0 or not params[0]:
			completions = [u'color', u'log-file', u'log-level']

		elif paramPos == 1:
			if u'color'.startswith(params[0]):
				completions = [u'color']
			if u'log-file'.startswith(params[0]):
				completions = [u'log-file']
			if u'log-level'.startswith(params[0]):
				completions.append(u'log-level')

		elif paramPos == 2:
			if params[0] == u'color':
				completions = [u'on', u'off']
			elif params[0] == u'log-file':
				completions = [u'<filename>', u'off']
			elif params[0] == u'log-level':
				completions = [u'0', u'1', u'2', u'3', u'4', u'5', u'6', u'7', u'8', u'9']

		return completions

	def execute(self, shell, params):
		global logFile
		global logLevel

		if len(params) <= 0:
			raise Exception(_(u'Missing option'))
		if params[0] not in (u'color', u'log-file', u'log-level'):
			raise Exception(_(u'Unknown option: %s') % params[0])
		if len(params) <= 1:
			raise Exception(_(u'Missing value'))

		if params[0] == u'color':
			if params[1] == u'on':
				shell.setColor(True)
			elif params[1] == u'off':
				shell.setColor(False)
			else:
				raise Exception(_(u'Bad value: %s') % params[1])

		elif params[0] == u'log-file':
			if params[1] == u'off':
				logger.setFileLevel(LOG_NONE)
				logger.setLogFile(None)
			else:
				logFile = params[1]
				startLogFile(logFile)

		elif params[0] == u'log-level':
			if not logFile:
				raise Exception(_(u'No log-file set!'))
			logger.setFileLevel(int(params[1]))


class CommandHelp(Command):
	def __init__(self):
		Command.__init__(self, u'help')

	def getDescription(self):
		return _(u"Show this text")

	def execute(self, shell, params):
		shell.appendLine(u'\r' + _(u"Commands are:") + u'\n', refresh=False)
		for cmd in shell.commands:
			shell.appendLine(u"\r\t%-20s%s\n" % (cmd.getName() + ':', cmd.getDescription()), refresh=False)
		shell.display()


class CommandQuit(Command):
	def __init__(self):
		Command.__init__(self, u'quit')

	def getDescription(self):
		return _(u"Exit opsi-admin")

	def execute(self, shell, params):
		shell.exit()


class CommandExit(CommandQuit):
	def __init__(self):
		Command.__init__(self, u'exit')


class CommandHistory(Command):
	def __init__(self):
		Command.__init__(self, u'history')

	def getDescription(self):
		return _(u"show / clear command history")

	def completion(self, params, paramPos):
		completions = []

		if paramPos == 0 or not params[0]:
			completions = [u'clear', u'show']

		elif paramPos == 1:
			if u'clear'.startswith(params[0]):
				completions = [u'clear']
			elif u'show'.startswith(params[0]):
				completions = [u'show']

		return completions

	def execute(self, shell, params):
		if len(params) <= 0:
			# By default: show history
			params = [u'show']
		elif params[0] not in (u'clear', u'show'):
			raise Exception(_(u'Unknown command: %s') % params[0])

		if params[0] == u'show':
			for line in shell.cmdList:
				shell.appendLine(line, refresh=False)
			shell.display()
		elif params[0] == u'clear':
			shell.cmdList = []
			shell.cmdListPos = -1


class CommandLog(Command):
	def __init__(self):
		Command.__init__(self, u'log')

	def getDescription(self):
		return _(u"show log")

	def completion(self, params, paramPos):
		completions = []

		if paramPos == 0 or not params[0]:
			completions = [u'show']

		elif paramPos == 1:
			if u'show'.startswith(params[0]):
				completions = [u'show']

		return completions

	def execute(self, shell, params):
		if len(params) <= 0:
			# By default: show log
			params = [u'show']
		elif params[0] not in (u'show',):
			raise Exception(_(u'Unknown command: %s') % params[0])

		if params[0] == u'show':
			if not logFile:
				raise Exception(_(u'File logging is not activated'))

			with open(logFile) as log:
				for line in log:
					shell.appendLine(line, refresh=False)
			shell.display()


class CommandTask(Command):
	def __init__(self):
		Command.__init__(self, u'task')
		self._tasks = (
			(u'setupWhereInstalled', u'productId'),
			(u'setupWhereNotInstalled', u'productId'),
			(u'updateWhereInstalled', 'productId'),
			(u'uninstallWhereInstalled', 'productId'),
			(u'setActionRequestWhereOutdated', 'actionRequest', 'productId'),
			(u'setActionRequestWhereOutdatedWithDependencies', 'actionRequest', 'productId'),
			(u'setActionRequestWithDependencies', 'actionRequest', 'productId', 'clientId'),
			(u'decodePcpatchPassword', u'encodedPassword', u'opsiHostKey'),
			(u'setPcpatchPassword', u'*password')
		)

	def getDescription(self):
		return _(u"execute a task")

	def help(self, shell):
		shell.appendLine(u'')

	def completion(self, params, paramPos):
		completions = []

		if paramPos == 0 or not params[0]:
			for task in self._tasks:
				completions.append(task[0])

		elif paramPos == 1:
			for task in self._tasks:
				if task[0].startswith(params[0]):
					completions.append(task[0])

		elif paramPos >= 2:
			for task in self._tasks:
				if params[0] == task[0] and paramPos <= len(task):
					completions.append(task[paramPos - 1])

		return completions

	def execute(self, shell, params):
		tasknames = set([task[0] for task in self._tasks])

		if len(params) <= 0:
			return
		elif params[0] not in tasknames:
			raise Exception(_(u'Unknown task: %s') % params[0])

		if params[0] == u'setupWhereInstalled':
			if len(params) < 2:
				raise Exception(_(u'Missing product-id'))
			productId = params[1]
			products = backend.product_getObjects(id=productId)
			if not products:
				raise Exception(_(u'Given productId not found'))
			else:
				if products[0].getSetupScript():
					clientIds = backend.getClientIds_list(productId=productId, installationStatus=u'installed')
					if clientIds:
						for clientId in clientIds:
							shell.appendLine(clientId)
							backend.setProductActionRequest(productId, clientId, 'setup')

		elif params[0] == u'setupWhereNotInstalled':
			if len(params) < 2:
				raise Exception(_(u'Missing product-id'))
			productId = params[1]
			products = backend.product_getObjects(id=productId)
			if not products:
				raise Exception(_(u'Given productId not found'))
			else:
				if products[0].getSetupScript():
					clientIds = backend.getClientIds_list(productId=productId, installationStatus=u'not_installed')
					if clientIds:
						for clientId in clientIds:
							shell.appendLine(clientId)
							backend.setProductActionRequest(productId, clientId, u'setup')

		elif params[0] == u'updateWhereInstalled':
			if len(params) < 2:
				raise Exception(_(u'Missing product-id'))
			productId = params[1]
			products = backend.product_getObjects(id=productId)
			if not products:
				raise Exception(_(u'Given productId not found'))

			if products[0].getUpdateScript():
				clientIds = backend.getClientIds_list(productId=productId, installationStatus=u'installed')

				for clientId in clientIds:
					shell.appendLine(clientId)
					backend.setProductActionRequest(productId, clientId, u'update')

		elif params[0] == u'uninstallWhereInstalled':
			if len(params) < 2:
				raise Exception(_(u'Missing product-id'))
			productId = params[1]
			products = backend.product_getObjects(id=productId)
			if not products:
				raise Exception(_(u'Given productId not found'))
			else:
				if products[0].getUninstallScript():
					clientIds = backend.getClientIds_list(productId=productId, installationStatus=u'installed')
					if clientIds:
						for clientId in clientIds:
							shell.appendLine(clientId)
							backend.setProductActionRequest(productId, clientId, u'uninstall')

		elif params[0] == u'setActionRequestWhereOutdated':
			if len(params) < 2:
				raise Exception(_(u'Missing action request'))
			if len(params) < 3:
				raise Exception(_(u'Missing product-id'))
			actionRequest = params[1]
			productId = params[2]

			# Get depot to client assignment
			depotToClients = {}
			for clientToDepot in backend.configState_getClientToDepotserver():
				try:
					depotToClients[clientToDepot['depotId']].append(clientToDepot['clientId'])
				except KeyError:
					depotToClients[clientToDepot['depotId']] = [clientToDepot['clientId']]

			updateProductOnClients = []
			for _depot, clientIds in depotToClients.items():
				if not clientIds:
					continue

				for productOnDepot in backend.productOnDepot_getObjects(productId=productId):
					for productOnClient in backend.productOnClient_getObjects(clientId=clientIds, productId=productOnDepot.productId, installationStatus=u'installed'):
						if (productOnClient.productVersion != productOnDepot.productVersion) or (productOnClient.packageVersion != productOnDepot.packageVersion):
							productOnClient.setActionRequest(actionRequest)
							updateProductOnClients.append(productOnClient)
							shell.appendLine(productOnClient.clientId)

			backend.productOnClient_updateObjects(updateProductOnClients)

		elif params[0] == u'setActionRequestWithDependencies':
			if len(params) < 2:
				raise Exception(_(u'Missing action request'))
			if len(params) < 3:
				raise Exception(_(u'Missing product-id'))
			if len(params) < 4:
				raise Exception(_(u'Missing client-id'))
			actionRequest = params[1]
			productId = params[2]
			clientId = params[3]

			if productId and clientId and actionRequest:
				backend.setProductActionRequestWithDependencies(productId, clientId, actionRequest)

		elif params[0] == u'setActionRequestWhereOutdatedWithDependencies':
			if len(params) < 2:
				raise Exception(_(u'Missing action request'))
			if len(params) < 3:
				raise Exception(_(u'Missing product-id'))

			actionRequest = params[1]
			productId = params[2]

			# Get depot to client assignment
			depotToClients = {}
			for clientToDepot in backend.configState_getClientToDepotserver():
				try:
					depotToClients[clientToDepot['depotId']].append(clientToDepot['clientId'])
				except KeyError:
					depotToClients[clientToDepot['depotId']] = [clientToDepot['clientId']]

			updateProductOnClients = []
			for _depot, clientIds in depotToClients.items():
				if not clientIds:
					continue

				for productOnDepot in backend.productOnDepot_getObjects(productId=productId):
					for productOnClient in backend.productOnClient_getObjects(clientId=clientIds, productId=productOnDepot.productId, installationStatus=u'installed'):
						if productOnClient.productVersion != productOnDepot.productVersion or productOnClient.packageVersion != productOnDepot.packageVersion:
							productOnClient.setActionRequestWithDependencies(actionRequest)
							updateProductOnClients.append(productOnClient)
							shell.appendLine(productOnClient.clientId)

			if updateProductOnClients:
				backend.productOnClient_updateObjects(updateProductOnClients)

		elif params[0] == u'decodePcpatchPassword':
			if len(params) < 3:
				raise Exception(_(u'Missing argument'))
			crypt = params[1]
			key = params[2]
			cleartext = blowfishDecrypt(key, crypt)
			shell.appendLine(cleartext)

		elif params[0] == u'setPcpatchPassword':
			if os.getuid() != 0:
				raise Exception(_(u"You have to be root to change pcpatch password!"))

			fqdn = getfqdn(conf='/etc/opsi/global.conf')
			if fqdn.count('.') < 2:
				raise Exception(_(u"Failed to get my own fully qualified domainname"))

			password = u''
			if len(params) < 2:
				password = shell.getPassword()
			else:
				password = params[1]

			if not password:
				raise ValueError("Can not use empty password!")
			logger.addConfidentialString(password)

			backend.user_setCredentials(username='pcpatch', password=password)

			try:
				# Univention
				dn = None
				process = os.popen(u'%s users/user list --filter "(uid=pcpatch)"' % which('univention-admin'), 'r')
				for line in process.readlines():
					if line.startswith('DN'):
						dn = line.strip().split(' ')[1]
						break
				process.close()
				if not dn:
					raise Exception(u"Failed to get DN for user pcpatch")
				cmd = u"%s users/user modify --dn %s --set password='%s' --set overridePWLength=1 --set overridePWHistory=1 1>/dev/null 2>/dev/null" \
						% (which('univention-admin'), dn, password)
				os.system(cmd.encode(outEncoding))
			except Exception:
				try:
					# smbldap
					process = os.popen(u'%s pcpatch 1>/dev/null 2>/dev/null' % which('smbldap-passwd'), 'w')
					process.write((u"%s\n%s\n" % (password, password)).encode(outEncoding))
					process.close()
				except Exception:
					# unix
					process = os.popen(u'%s 1>/dev/null 2>/dev/null' % which(u'chpasswd'), 'w')
					process.write((u"pcpatch:%s\n" % password).encode(outEncoding))
					process.close()

					process = os.popen(u'%s -a -s pcpatch 1>/dev/null 2>/dev/null' % which('smbpasswd'), 'w')
					process.write((u"%s\n%s\n" % (password, password)).encode(outEncoding))
					process.close()

if __name__ == "__main__":
	def shellExit():
		try:
			shell.exit()
		except Exception:
			pass

	try:
		locale.setlocale(locale.LC_ALL, '')
	except Exception:
		pass

	if os.name == 'posix':
		from signal import signal, SIGINT, SIGQUIT
		signal(SIGINT, signalHandler)
		signal(SIGQUIT, signalHandler)

	try:
		main(sys.argv[1:])
		shellExit()
		exitCode = 0
	except ErrorInResultException as error:
		shellExit()
		logger.warning(u"Error in result: {0}".format(forceUnicode(error)))
		exitCode = 2
	except Exception as exception:
		shellExit()
		logger.setConsoleLevel(LOG_ERROR)
		logger.logException(exception)
		exitCode = 1

	if exitZero:
		exitCode = 0

	sys.exit(exitCode)
