#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Copyright 2004-2022 Univention GmbH
#
# https://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
# <https://www.gnu.org/licenses/>.
"""
command line frontend to univention-directory-manager (module)
"""
from __future__ import print_function
import getopt
import base64
import os
import subprocess
import traceback
from ipaddress import IPv4Address, IPv4Network
import ldap
import six
import univention.debug as ud
import univention.admin.uexceptions
import univention.admin.uldap
import univention.admin.modules
import univention.admin.objects
from univention.admin.layout import Group
from univention.admin.syntax import ldapFilter
import univention.config_registry
univention.admin.modules.update()
[docs]class OperationFailed(Exception):
	def __init__(self, out=None, msg=None):
		self.out = out or []
		if msg:
			self.out.append(msg) 
[docs]def usage():
	out = []
	out.append('univention-directory-manager: command line interface for managing UCS')
	out.append('copyright (c) 2001-@%@copyright_lastyear@%@ Univention GmbH, Germany')
	out.append('')
	out.append('Syntax:')
	out.append('  univention-directory-manager module action [options]')
	out.append('  univention-directory-manager [--help] [--version]')
	out.append('')
	out.append('actions:')
	out.append('  %-32s %s' % ('create:', 'Create a new object'))
	out.append('  %-32s %s' % ('modify:', 'Modify an existing object'))
	out.append('  %-32s %s' % ('remove:', 'Remove an existing object'))
	out.append('  %-32s %s' % ('list:', 'List objects'))
	out.append('  %-32s %s' % ('move:', 'Move object in directory tree'))
	out.append('')
	out.append('  %-32s %s' % ('-h | --help | -?:', 'print this usage message'))
	out.append('  %-32s %s' % ('--version:', 'print version information'))
	out.append('')
	out.append('general options:')
	out.append('  --%-30s %s' % ('binddn', 'bind DN'))
	out.append('  --%-30s %s' % ('bindpwd', 'bind password'))
	out.append('  --%-30s %s' % ('bindpwdfile', 'file containing bind password'))
	out.append('  --%-30s %s' % ('logfile', 'path and name of the logfile to be used'))
	out.append('  --%-30s %s' % ('tls', '0 (no); 1 (try); 2 (must)'))
	out.append('')
	out.append('create options:')
	out.append('  --%-30s %s' % ('position', 'Set position in tree'))
	out.append('  --%-30s %s' % ('set', 'Set variable to value, e.g. foo=bar'))
	out.append('  --%-30s %s' % ('superordinate', 'Use superordinate module'))
	out.append('  --%-30s %s' % ('option', 'Use only given module options'))
	out.append('  --%-30s %s' % ('append-option', 'Append the module option'))
	out.append('  --%-30s %s' % ('remove-option', 'Remove the module option'))
	out.append('  --%-30s %s' % ('policy-reference', 'Reference to policy given by DN'))
	out.append('  --%-30s   ' % ('ignore_exists'))
	out.append('')
	out.append('modify options:')
	out.append('  --%-30s %s' % ('dn', 'Edit object with DN'))
	out.append('  --%-30s %s' % ('set', 'Set variable to value, e.g. foo=bar'))
	out.append('  --%-30s %s' % ('append', 'Append value to variable, e.g. foo=bar'))
	out.append('  --%-30s %s' % ('remove', 'Remove value from variable, e.g. foo=bar'))
	out.append('  --%-30s %s' % ('option', 'Use only given module options'))
	out.append('  --%-30s %s' % ('append-option', 'Append the module option'))
	out.append('  --%-30s %s' % ('remove-option', 'Remove the module option'))
	out.append('  --%-30s %s' % ('policy-reference', 'Reference to policy given by DN'))
	out.append('  --%-30s %s' % ('policy-dereference', 'Remove reference to policy given by DN'))
	out.append('  --%-30s   ' % ('ignore_not_exists'))
	out.append('')
	out.append('remove options:')
	out.append('  --%-30s %s' % ('dn', 'Remove object with DN'))
	out.append('  --%-30s %s' % ('superordinate', 'Use superordinate module'))
	out.append('  --%-30s %s' % ('filter', 'Lookup filter e.g. foo=bar'))
	out.append('  --%-30s %s' % ('remove_referring', 'remove referring objects'))
	out.append('  --%-30s   ' % ('ignore_not_exists'))
	out.append('')
	out.append('list options:')
	out.append('  --%-30s %s' % ('filter', 'Lookup filter e.g. foo=bar'))
	out.append('  --%-30s %s' % ('position', 'Search underneath of position in tree'))
	out.append('  --%-30s %s' % ('policies', 'List policy-based settings:'))
	out.append('    %-30s %s' % ('', '0:short, 1:long (with policy-DN)'))
	out.append('')
	out.append('move options:')
	out.append('  --%-30s %s' % ('dn', 'Move object with DN'))
	out.append('  --%-30s %s' % ('position', 'Move to position in tree'))
	out.append('')
	out.append('Description:')
	out.append('  univention-directory-manager is a tool to handle the configuration for UCS')
	out.append('  on command line level.')
	out.append('  Use "univention-directory-manager modules" for a list of available modules.')
	out.append('')
	return out 
[docs]def version():
	o = []
	o.append('univention-directory-manager @%@package_version@%@')
	return o 
def _print_property(module, action, name, output):
	property = module.property_descriptions.get(name)
	if property is None:
		output.append('E: unknown property %s of module %s' % (name, univention.admin.modules.name(module)))
		return
	required = {
		'create': False,
		'modify': False,
		'remove': False,
		'editable': True,
	}
	if property.required:
		required['create'] = True
	if property.identifies:
		required['modify'] = True
		required['remove'] = True
	if not property.editable:
		required['modify'] = False
		required['remove'] = False
		required['editable'] = False
	flags = ''
	if action in required and required[action]:
		flags = '*'
	elif action not in required:
		if required['create']:
			flags += 'c'
		if required['modify']:
			flags += 'm'
		if required['remove']:
			flags += 'r'
		if not required['editable']:
			flags += 'e'
	if property.options:
		if flags:
			flags += ','
		flags += ','.join(property.options)
	if property.multivalue:
		if flags:
			flags += ','
		flags += '[]'
	if flags:
		flags = '(' + flags + ')'
	output.append('		%-40s %s' % (name + ' ' + flags, property.short_description))
[docs]def module_usage(information, action=''):
	out = []
	for module, l in information.items():
		properties, options = l
		if options:
			out.append('')
			out.append('%s options:' % module.module)
			for name, option in options.items():
				out.append('  %-32s %s' % (name, option.short_description))
		out.append('')
		out.append('%s variables:' % module.module)
		if not hasattr(module, "layout"):
			continue
		for moduletab in module.layout:
			out.append('  %s:' % (moduletab.label))
			for row in moduletab.layout:
				if isinstance(row, Group):
					out.append('	%s' % row.label)
					for row in row.layout:
						if isinstance(row, six.string_types):
							_print_property(module, action, row, out)
							continue
						for item in row:
							_print_property(module, action, item, out)
				else:
					if isinstance(row, six.string_types):
						_print_property(module, action, row, out)
						continue
					for item in row:
						_print_property(module, action, item, out)
	return out 
[docs]def list_available_modules(o=[]):
	o.append("Available Modules are:")
	for mod in sorted(univention.admin.modules.modules):
		o.append("  %s" % mod)
	return o 
[docs]def doit(arglist):
	out = []
	try:
		out = _doit(arglist)
	except OperationFailed as exc:
		return exc.out + ["OPERATION FAILED"]
	except ldap.SERVER_DOWN:
		return out + ["E: The LDAP Server is currently not available.", "OPERATION FAILED"]
	except univention.admin.uexceptions.base as e:
		ud.debug(ud.ADMIN, ud.WARN, str(e))
		# collect error information
		msg = []
		if getattr(e, 'message', None):
			msg.append(e.message)
		if getattr(e, 'args', None):
			# avoid duplicate messages
			if not len(msg) or len(e.args) > 1 or e.args[0] != msg[0]:
				msg.extend(e.args)
		# strip elements and make sure that a ':' is printed if further information follows
		msg = [i.strip() for i in msg]
		if len(msg) == 1:
			msg[0] = '%s.' % msg[0].strip(':.')
		elif len(msg) > 1:
			msg[0] = '%s:' % msg[0].strip(':.')
		# append to the output
		out.append(' '.join(msg))
		return out + ["OPERATION FAILED"]
	except BaseException:
		ud.debug(ud.ADMIN, ud.ERROR, traceback.format_exc())
		raise
	return out 
def _doit(arglist):
	out = []
	# parse module and action
	if len(arglist) < 2:
		raise OperationFailed(usage())
	module_name = arglist[1]
	if module_name in ['-h', '--help', '-?']:
		return usage()
	if module_name == '--version':
		return version()
	if module_name == 'modules':
		return list_available_modules()
	remove_referring = 0
	recursive = 1
	# parse options
	longopts = ['position=', 'dn=', 'set=', 'append=', 'remove=', 'superordinate=', 'option=', 'append-option=', 'remove-option=', 'filter=', 'tls=', 'ignore_exists', 'ignore_not_exists', 'logfile=', 'policies=', 'binddn=', 'bindpwd=', 'bindpwdfile=', 'policy-reference=', 'policy-dereference=', 'remove_referring', 'recursive']
	try:
		opts, args = getopt.getopt(arglist[3:], '', longopts)
	except getopt.error as msg:
		raise OperationFailed(out, str(msg))
	if args and isinstance(args, list):
		msg = "WARNING: the following arguments are ignored:"
		for argument in args:
			msg = '%s "%s"' % (msg, argument)
		out.append(msg)
	position_dn = ''
	dn = ''
	binddn = None
	bindpwd = None
	list_policies = False
	policies_with_DN = False
	policyOptions = []
	logfile = '/var/log/univention/directory-manager-cmd.log'
	tls = 2
	ignore_exists = 0
	ignore_not_exists = False
	superordinate_dn = ''
	parsed_append_options = []
	parsed_remove_options = []
	parsed_options = []
	filter = ''
	input = {}
	append = {}
	remove = {}
	policy_reference = []
	policy_dereference = []
	for opt, val in opts:
		if opt == '--position':
			position_dn = val
		elif opt == '--logfile':
			logfile = val
		elif opt == '--policies':
			list_policies = True
			if val == "1":
				policies_with_DN = True
			else:
				policyOptions = ['-s']
		elif opt == '--binddn':
			binddn = val
		elif opt == '--bindpwd':
			bindpwd = val
		elif opt == '--bindpwdfile':
			try:
				with open(val) as fp:
					bindpwd = fp.read().strip()
			except IOError as exc:
				raise OperationFailed(out, 'E: could not read bindpwd from file (%s)' % (exc,))
		elif opt == '--dn':
			dn = val
		elif opt == '--tls':
			tls = val
		elif opt == '--ignore_exists':
			ignore_exists = 1
		elif opt == '--ignore_not_exists':
			ignore_not_exists = True
		elif opt == '--superordinate':
			superordinate_dn = val
		elif opt == '--option':
			parsed_options.append(val)
		elif opt == '--append-option':
			parsed_append_options.append(val)
		elif opt == '--remove-option':
			parsed_remove_options.append(val)
		elif opt == '--filter':
			ldapFilter.parse(val)
			filter = val
		elif opt == '--policy-reference':
			policy_reference.append(val)
		elif opt == '--policy-dereference':
			policy_dereference.append(val)
	if logfile:
		ud.init(logfile, ud.FLUSH, ud.NO_FUNCTION)
	else:
		out.append("WARNING: no logfile specified")
	configRegistry = univention.config_registry.ConfigRegistry()
	configRegistry.load()
	baseDN = configRegistry['ldap/base']
	debug_level = int(configRegistry.get('directory/manager/cmd/debug/level', 0))
	ud.set_level(ud.LDAP, debug_level)
	ud.set_level(ud.ADMIN, debug_level)
	if binddn and bindpwd:
		ud.debug(ud.ADMIN, ud.INFO, "using %s account" % binddn)
		try:
			lo = univention.admin.uldap.access(host=configRegistry['ldap/master'], port=int(configRegistry.get('ldap/master/port', '7389')), base=baseDN, binddn=binddn, start_tls=tls, bindpw=bindpwd)
		except Exception as exc:
			ud.debug(ud.ADMIN, ud.WARN, 'authentication error: %s' % (exc,))
			raise OperationFailed(out, 'authentication error: %s' % (exc,))
		policyOptions.extend(['-D', binddn, '-w', bindpwd])  # FIXME not so nice
	else:
		if os.path.exists('/etc/ldap.secret'):
			ud.debug(ud.ADMIN, ud.INFO, "using cn=admin,%s account" % baseDN)
			secretFileName = '/etc/ldap.secret'
			binddn = 'cn=admin,' + baseDN
			policyOptions.extend(['-D', binddn, '-y', secretFileName])
		elif os.path.exists('/etc/machine.secret'):
			ud.debug(ud.ADMIN, ud.INFO, "using %s account" % configRegistry['ldap/hostdn'])
			secretFileName = '/etc/machine.secret'
			binddn = configRegistry['ldap/hostdn']
			policyOptions.extend(['-D', binddn, '-y', secretFileName])
		try:
			with open(secretFileName, 'r') as secretFile:
				pwd = secretFile.read().strip('\n')
		except IOError:
			raise OperationFailed(out, 'E: Permission denied, try --binddn and --bindpwd')
		try:
			lo = univention.admin.uldap.access(host=configRegistry['ldap/master'], port=int(configRegistry.get('ldap/master/port', '7389')), base=baseDN, binddn=binddn, bindpw=pwd, start_tls=tls)
		except Exception as exc:
			ud.debug(ud.ADMIN, ud.WARN, 'authentication error: %s' % (exc,))
			raise OperationFailed(out, 'authentication error: %s' % (exc,))
	if not position_dn and superordinate_dn:
		position_dn = superordinate_dn
	elif not position_dn:
		position_dn = baseDN
	try:
		position = univention.admin.uldap.position(baseDN)
		position.setDn(position_dn)
	except univention.admin.uexceptions.noObject:
		raise OperationFailed(out, 'E: Invalid position')
	module = univention.admin.modules.get(module_name)
	if not module:
		out.append("unknown module %s." % module_name)
		out.append("")
		raise OperationFailed(list_available_modules(out))
	# initialise modules
	if module_name == 'settings/usertemplate':
		univention.admin.modules.init(lo, position, univention.admin.modules.get('users/user'))
	univention.admin.modules.init(lo, position, module)
	information = module_information(module)
	superordinate = None
	if superordinate_dn and univention.admin.modules.superordinate(module):
		# the superordinate itself also has a superordinate, get it!
		superordinate = univention.admin.objects.get_superordinate(module, None, lo, superordinate_dn)
		if superordinate is None:
			raise OperationFailed(out, 'E: %s is not a superordinate for %s.' % (superordinate_dn, univention.admin.modules.name(module)))
	if len(arglist) == 2:
		out = usage() + module_usage(information)
		raise OperationFailed(out)
	action = arglist[2]
	if len(arglist) == 3 and action != 'list':
		out = usage() + module_usage(information, action)
		raise OperationFailed(out)
	for opt, val in opts:
		if opt == '--set':
			name, delim, value = val.partition('=')
			for mod, (properties, options) in information.items():
				if name in properties:
					if not properties[name].cli_enabled:
						continue
					if properties[name].multivalue:
						input.setdefault(name, [])
						if value:
							input[name].append(value)
					else:
						input[name] = value
			if name not in input:
				out.append("WARNING: No attribute with name '%s' in this module, value not set." % name)
		elif opt == '--append':
			name, delim, value = val.partition('=')
			for mod, (properties, options) in information.items():
				if name in properties:
					if not properties[name].cli_enabled:
						continue
					if not properties[name].multivalue:
						out.append('WARNING: using --append on a single value property (%s). Use --set instead!' % (name,))
					append.setdefault(name, [])
					if value:
						append[name].append(value)
			if name not in append:
				out.append("WARNING: No attribute with name %s in this module, value not appended." % name)
		elif opt == '--remove':
			name, delim, value = val.partition('=')
			value = value or None
			for mod, (properties, options) in information.items():
				if name in properties:
					if not properties[name].cli_enabled:
						continue
					if properties[name].multivalue:
						if value is None:
							remove[name] = value
						elif value:
							remove.setdefault(name, [])
							if remove[name] is not None:
								remove[name].append(value)
					else:
						remove[name] = value
			if name not in remove:
				out.append("WARNING: No attribute with name %s in this module, value not removed." % name)
		elif opt == '--remove_referring':
			remove_referring = True
		elif opt == '--recursive':
			recursive = True
	cli = CLI(module_name, module, dn, lo, position, superordinate)
	if action == 'create' or action == 'new':
		out.extend(cli.create(input, append, ignore_exists, parsed_options, parsed_append_options, parsed_remove_options, policy_reference))
	elif action == 'modify' or action == 'edit':
		out.extend(cli.modify(input, append, remove, parsed_append_options, parsed_remove_options, parsed_options, policy_reference, policy_dereference, ignore_not_exists=ignore_not_exists))
	elif action == 'move':
		out.extend(cli.move(position_dn))
	elif action == 'remove' or action == 'delete':
		out.extend(cli.remove(remove_referring=remove_referring, recursive=recursive, ignore_not_exists=ignore_not_exists, filter=filter))
	elif action == 'list' or action == 'lookup':
		out.extend(cli.list(list_policies, filter, superordinate_dn, policyOptions, policies_with_DN))
	else:
		out.append("Unknown or no action defined")
		out.append('')
		raise OperationFailed(out)
	return out  # nearly the only successful return
[docs]class CLI(object):
	def __init__(self, module_name, module, dn, lo, position, superordinate):
		self.module_name = module_name
		self.module = module
		self.dn = dn
		self.lo = lo
		self.position = position
		self.superordinate = superordinate
[docs]	def create(self, *args, **kwargs):
		return self._create(self.module_name, self.module, self.dn, self.lo, self.position, self.superordinate, *args, **kwargs) 
[docs]	def modify(self, *args, **kwargs):
		return self._modify(self.module_name, self.module, self.dn, self.lo, self.position, self.superordinate, *args, **kwargs) 
[docs]	def move(self, *args, **kwargs):
		return self._move(self.module_name, self.module, self.dn, self.lo, self.position, self.superordinate, *args, **kwargs) 
[docs]	def remove(self, *args, **kwargs):
		return self._remove(self.module_name, self.module, self.dn, self.lo, self.position, self.superordinate, *args, **kwargs) 
[docs]	def list(self, *args, **kwargs):
		return self._list(self.module_name, self.module, self.dn, self.lo, self.position, self.superordinate, *args, **kwargs) 
	def _create(self, module_name, module, dn, lo, position, superordinate, input, append, ignore_exists, parsed_options, parsed_append_options, parsed_remove_options, policy_reference):
		out = []
		if not univention.admin.modules.supports(module_name, 'add'):
			raise OperationFailed(out, 'Create %s not allowed' % module_name)
		try:
			object = module.object(None, lo, position=position, superordinate=superordinate)
		except univention.admin.uexceptions.insufficientInformation as exc:
			raise OperationFailed(out, 'E: Insufficient information: %s' % (exc,))
		if parsed_options:
			object.options = parsed_options
		for option in parsed_append_options:
			object.options.append(option)
		for option in parsed_remove_options:
			try:
				object.options.remove(option)
			except ValueError:
				pass
		object.open()
		try:
			out.extend(object_input(module, object, input, append=append))
		except univention.admin.uexceptions.nextFreeIp:
			if not ignore_exists:
				raise OperationFailed(out, 'E: No free IP address found')
		except univention.admin.uexceptions.valueInvalidSyntax as err:
			raise OperationFailed(out, 'E: Invalid Syntax: %s' % err)
		default_containers = object.get_default_containers(lo)
		if default_containers and position.isBase() and not any(lo.compare_dn(default_container, position.getDn()) for default_container in default_containers):
			out.append('WARNING: The object is not going to be created underneath of its default containers.')
		object.policy_reference(*policy_reference)
		exists = False
		exists_msg = None
		created = False
		try:
			dn = object.create()
			created = True
		except univention.admin.uexceptions.objectExists as exc:
			exists_msg = '%s' % (exc,)
			dn = exc.args[0]
			if not ignore_exists:
				raise OperationFailed(out, 'E: Object exists: %s' % exists_msg)
			else:
				exists = 1
		except univention.admin.uexceptions.uidAlreadyUsed as user:
			exists_msg = '(uid) %s' % user
			if not ignore_exists:
				raise OperationFailed(out, 'E: Object exists: %s' % exists_msg)
			else:
				exists = 1
		except univention.admin.uexceptions.groupNameAlreadyUsed as group:
			exists_msg = '(group) %s' % group
			if not ignore_exists:
				raise OperationFailed(out, 'E: Object exists: %s' % exists_msg)
			else:
				exists = 1
		except univention.admin.uexceptions.dhcpServerAlreadyUsed as name:
			exists_msg = '(dhcpserver) %s' % name
			if not ignore_exists:
				raise OperationFailed(out, 'E: Object exists: %s' % exists_msg)
			else:
				exists = 1
		except univention.admin.uexceptions.macAlreadyUsed as mac:
			exists_msg = '(mac) %s' % mac
			if not ignore_exists:
				raise OperationFailed(out, 'E: Object exists: %s' % exists_msg)
			else:
				exists = 1
		except univention.admin.uexceptions.noLock as e:
			exists_msg = '(nolock) %s' % (e,)
			if not ignore_exists:
				raise OperationFailed(out, 'E: Object exists: %s' % exists_msg)
			else:
				exists = 1
		except univention.admin.uexceptions.invalidDhcpEntry:
			raise OperationFailed(out, 'E: The DHCP entry for this host should contain the zone dn, the ip address and the mac address.')
		except univention.admin.uexceptions.invalidOptions as e:
			out.append('E: invalid Options: %s' % e)
			if not ignore_exists:
				raise OperationFailed(out)
		except univention.admin.uexceptions.insufficientInformation as exc:
			raise OperationFailed(out, 'E: Insufficient information: %s' % (exc,))
		except univention.admin.uexceptions.noObject as e:
			raise OperationFailed(out, 'E: object not found: %s' % e)
		except univention.admin.uexceptions.circularGroupDependency as e:
			raise OperationFailed(out, 'E: circular group dependency detected: %s' % e)
		except univention.admin.uexceptions.invalidChild as e:
			raise OperationFailed(out, 'E: %s' % e)
		if exists:
			if exists_msg:
				out.append('Object exists: %s' % exists_msg)
			else:
				out.append('Object exists')
		elif created:
			out.append('Object created: %s' % dn)
		return out
	def _move(self, module_name, module, dn, lo, position, superordinate, position_dn):
		out = []
		if not dn:
			raise OperationFailed(out, 'E: DN is missing')
		object_modified = 0
		if not univention.admin.modules.supports(module_name, 'edit'):
			raise OperationFailed(out, 'Modify %s not allowed' % module_name)
		try:
			object = univention.admin.objects.get(module, None, lo, position='', dn=dn)
		except univention.admin.uexceptions.noObject:
			raise OperationFailed(out, 'E: object not found')
		object.open()
		if not univention.admin.modules.supports(module_name, 'move'):
			raise OperationFailed(out, 'Move %s not allowed' % module_name)
		if not position_dn:
			out.append("need new position for moving object")
		else:
			try:  # check if destination exists
				lo.get(position_dn, required=True)
			except (univention.admin.uexceptions.noObject, ldap.INVALID_DN_SYNTAX):
				raise OperationFailed(out, "position does not exists: %s" % position_dn)
			rdn = ldap.dn.dn2str([ldap.dn.str2dn(dn)[0]])
			newdn = "%s,%s" % (rdn, position_dn)
			try:
				object.move(newdn)
				object_modified += 1
			except univention.admin.uexceptions.noObject:
				raise OperationFailed(out, 'E: object not found')
			except univention.admin.uexceptions.ldapError as msg:
				raise OperationFailed(out, "ldap Error: %s" % msg)
			except univention.admin.uexceptions.nextFreeIp:
				raise OperationFailed(out, 'E: No free IP address found')
			except univention.admin.uexceptions.valueInvalidSyntax as err:
				raise OperationFailed(out, 'E: Invalid Syntax: %s' % err)
			except univention.admin.uexceptions.invalidOperation as msg:
				raise OperationFailed(out, str(msg))
		if object_modified > 0:
			out.append('Object modified: %s' % dn)
		else:
			out.append('No modification: %s' % dn)
		return out
	def _modify(self, module_name, module, dn, lo, position, superordinate, input, append, remove, parsed_append_options, parsed_remove_options, parsed_options, policy_reference, policy_dereference, ignore_not_exists):
		out = []
		if not dn:
			raise OperationFailed(out, 'E: DN is missing')
		object_modified = 0
		if not univention.admin.modules.supports(module_name, 'edit'):
			raise OperationFailed(out, 'Modify %s not allowed' % module_name)
		try:
			object = univention.admin.objects.get(module, None, lo, position='', dn=dn)
		except univention.admin.uexceptions.noObject:
			if ignore_not_exists:
				out.append('Object not found: %s' % (dn or filter,))
				return out
			raise OperationFailed(out, 'E: object not found')
		object.open()
		if (len(input) + len(append) + len(remove) + len(parsed_append_options) + len(parsed_remove_options) + len(parsed_options) + len(policy_reference) + len(policy_dereference)) > 0:
			if parsed_options:
				object.options = parsed_options
			for option in parsed_append_options:
				object.options.append(option)
			for option in parsed_remove_options[:]:
				try:
					object.options.remove(option)
				except ValueError:
					parsed_remove_options.remove(option)
					out.append('WARNING: option %r is not set. Ignoring.' % (option,))
			try:
				out.extend(object_input(module, object, input, append, remove))
			except univention.admin.uexceptions.valueMayNotChange as exc:
				raise OperationFailed(out, str(exc))
			object.policy_reference(*policy_reference)
			object.policy_dereference(*policy_dereference)
			if object.hasChanged(input.keys()) or object.hasChanged(append.keys()) or object.hasChanged(remove.keys()) or parsed_append_options or parsed_remove_options or parsed_options or object.policiesChanged():
				try:
					dn = object.modify()
					object_modified += 1
				except univention.admin.uexceptions.noObject:
					raise OperationFailed(out, 'E: object not found')
				except univention.admin.uexceptions.invalidDhcpEntry:
					raise OperationFailed(out, 'E: The DHCP entry for this host should contain the zone dn, the ip address and the mac address.')
				except univention.admin.uexceptions.circularGroupDependency as e:
					raise OperationFailed(out, 'E: circular group dependency detected: %s' % e)
				except univention.admin.uexceptions.valueInvalidSyntax as e:
					raise OperationFailed(out, 'E: Invalid Syntax: %s' % e)
		if object_modified > 0:
			out.append('Object modified: %s' % dn)
		else:
			out.append('No modification: %s' % dn)
		return out
	def _remove(self, module_name, module, dn, lo, position, superordinate, recursive, remove_referring, ignore_not_exists, filter):
		out = []
		if not univention.admin.modules.supports(module_name, 'remove'):
			raise OperationFailed(out, 'Remove %s not allowed' % module_name)
		try:
			if dn and filter:
				object = univention.admin.modules.lookup(module, None, lo, scope='sub', superordinate=superordinate, base=dn, filter=filter, required=True, unique=True)[0]
			elif dn:
				object = univention.admin.modules.lookup(module, None, lo, scope='base', superordinate=superordinate, base=dn, filter=filter, required=True, unique=True)[0]
			elif filter:
				object = univention.admin.modules.lookup(module, None, lo, scope='sub', superordinate=superordinate, base=position.getDn(), filter=filter, required=True, unique=True)[0]
			else:
				raise OperationFailed(out, 'E: dn or filter needed')
		except (univention.admin.uexceptions.noObject, IndexError):
			if ignore_not_exists:
				out.append('Object not found: %s' % (dn or filter,))
				return out
			raise OperationFailed(out, 'E: object not found')
		object.open()
		if remove_referring and univention.admin.objects.wantsCleanup(object):
			univention.admin.objects.performCleanup(object)
		if recursive:
			try:
				object.remove(recursive)
			except univention.admin.uexceptions.ldapError as msg:
				raise OperationFailed(out, str(msg))
		else:
			try:
				object.remove()
			except univention.admin.uexceptions.primaryGroupUsed:
				raise OperationFailed(out, 'E: object in use')
		out.append('Object removed: %s' % (dn or object.dn,))
		return out
	def _list(self, module_name, module, dn, lo, position, superordinate, list_policies, filter, superordinate_dn, policyOptions, policies_with_DN):
		out = []
		if not univention.admin.modules.supports(module_name, 'search'):
			raise OperationFailed(out, 'Search %s not allowed' % module_name)
		out.append(filter)
		try:
			for object in univention.admin.modules.lookup(module, None, lo, scope='sub', superordinate=superordinate, base=position.getDn(), filter=filter):
				out.append('DN: %s' % univention.admin.objects.dn(object))
				if not univention.admin.modules.virtual(module_name):
					object.open()
					for key, value in sorted(object.items()):
						if not module.property_descriptions[key].show_in_lists:
							continue
						s = module.property_descriptions[key].syntax
						if module.property_descriptions[key].multivalue:
							for v in value:
								if s.tostring(v):
									out.append('  %s: %s' % (key, s.tostring(v)))
								else:
									out.append('  %s: %s' % (key, None))
						else:
							if s.tostring(value):
								if module.module == 'settings/portal' and key == 'content':
									out.append('  %s:\n  %s' % (key, s.tostring(value).replace('\n', '\n  ')))
								else:
									out.append('  %s: %s' % (key, s.tostring(value)))
							else:
								out.append('  %s: %s' % (key, None))
					for el in object.policies:
						out.append('  %s: %s' % ('univentionPolicyReference', el))
				if list_policies:
					policyResults = subprocess.check_output(['univention_policy_result'] + policyOptions + [univention.admin.objects.dn(object)], close_fds=True).decode('utf-8').split(u'\n')
					out.append("  Policy-based Settings:")
					policy = ''
					attribute = ''
					value = []
					client = {}
					for line in policyResults:
						line = line.strip()
						if not line or line.startswith("DN: ") or line.startswith("POLICY "):
							continue
						out.append("    %s" % line)
						if not policies_with_DN:
							ckey, cval = line.split('=', 1)
							client.setdefault(ckey, []).append(cval)
							continue
						ckey, cval = line.split(': ', 1)
						if ckey == 'Policy':
							if policy:
								client[attribute] = [policy, value]
								value = []
							policy = cval
						elif ckey == 'Attribute':
							attribute = cval
						elif ckey == 'Value':
							value.append(cval)
					if policies_with_DN:
						client[attribute] = [policy, value]
						value = []
					out.append('')
					if module_name == 'dhcp/host':
						subnet_module = univention.admin.modules.get('dhcp/subnet')
						# TODO: sharedsubnet_module = univention.admin.modules.get('dhcp/sharedsubnet')
						ips = object['fixedaddress']
						for ip in ips:
							ip_ = IPv4Address(u"%s" % (ip,))
							for subnet in univention.admin.modules.lookup(subnet_module, None, lo, scope='sub', superordinate=superordinate, base=superordinate_dn, filter=''):
								if ip_ in IPv4Network(u"%(subnet)s/%(subnetmask)s" % subnet, strict=False):
									policyResults = subprocess.check_output(['univention_policy_result'] + policyOptions + [subnet.dn], close_fds=True).decode('utf-8').split(u'\n')
									out.append("  Subnet-based Settings:")
									ddict = {}
									policy = ''
									value = []
									for line in policyResults:
										if not (line.strip() == "" or line.strip()[:4] == "DN: " or line.strip()[:7] == "POLICY "):
											out.append("    %s" % line.strip())
											if policies_with_DN:
												subsplit = line.strip().split(': ', 1)
												if subsplit[0] == 'Policy':
													if policy:
														ddict[attribute] = [policy, value]
														value = []
													policy = subsplit[1]
												elif subsplit[0] == 'Attribute':
													attribute = subsplit[1]
												elif subsplit[0] == 'Value':
													value.append(subsplit[1])
											else:
												subsplit = line.strip().split('=', 1)
												if subsplit[0] not in ddict:
													ddict[subsplit[0]] = []
												ddict[subsplit[0]].append(subsplit[1])
									out.append('')
									if policies_with_DN:
										ddict[attribute] = [policy, value]
										value = []
									out.append("  Merged Settings:")
									for key in ddict.keys():
										if key not in client:
											client[key] = ddict[key]
									if policies_with_DN:
										for key in client.keys():
											out.append("    Policy: " + client[key][0])
											out.append("    Attribute: " + key)
											for val in client[key][1]:
												out.append("    Value: " + val)
									else:
										for key in client.keys():
											for val in client[key]:
												out.append("    %s=%s" % (key, val))
									out.append('')
				out.append('')
		except univention.admin.uexceptions.ldapError as errmsg:
			raise OperationFailed(out, '%s' % (errmsg,))
		except univention.admin.uexceptions.valueInvalidSyntax as errmsg:
			raise OperationFailed(out, '%s' % (errmsg.message,))
		return out 
if __name__ == '__main__':
	import sys
	print('\n'.join(doit(sys.argv)))