
# $Id: config.py,v 1.22 2004/01/17 18:50:46 cran Exp $
#
# Copyright (c) 2002 Sebastian Stark
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR SEBASTIAN STARK
# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR
# OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


"""Configuration tree for tentakel

Provides a ConfigBase class that is initialized from a configuration file
with items that are derived from the UserDict.UserDict Python class.

Example:

  c = config.ConfigBase()
  c.load("tentakel.conf")
  availableGroups = c.getGroups()

or, if you want all hosts with expanded sublists:

  hosts = c.getExpandedGroup("mygroup")

In the latter case, only "host" objects are returned in a list.

"""

from error import *
import re
from UserDict import UserDict
import os
import pwd
import tempfile
import tpg

PARAMS = {	'ssh_path': "/usr/bin/ssh",
		'method': "ssh",
		'user': pwd.getpwuid(os.geteuid())[0],
		'format': r"### %d(%s):\n%o\n"
	}

METHODS = ['ssh']

class TConf(tpg.Parser):
	__doc__ = r"""

	set lexer = ContextSensitiveLexer

	token keyword	: '%(keywords)s'	str ;
	token eq	: '='			str ;
	token word	: '\w+'			str ;
	token vchar	: '""|[^"]'		str ;
	token hitem	: '\+[-\w\.:]+'		str ;
	token litem	: '@\w+'		str ;

	separator spaces	: '\s+' ;

	START/e ->			$ e = {"groups": {}, "settings": PARAMS}
		(	SETTING/s	$ e["settings"].update(s)
			| GROUP/g	$ e["groups"][g["name"]] = g
			| COMMENT
		)*
	;

	COMMENT -> 	@start '\s*#.*' @end
	;

	SETTING/s ->	'set'		$ s = {}
			PARAM/<p,v>	$ s[p] = v
	;

	PARAM/<p,v> ->	keyword/p eq
			'"'
			@start (vchar)* @end	$ t = self.extract(start, end)
			'"'			$ v = re.sub('""', '"', t)
	;

	GROUP/g ->      'group'		$ g = ConfigGroup()
			GROUPNAME/n	$ g["name"] = n
			GROUPSPEC/s	$ g.update(s)
			MEMBERS/l	$ g.update(l)
	;

	GROUPNAME/n -> word/n ;

	GROUPSPEC/s ->					$ s = {}
			'\('
				( PARAM/<p,v>		$ s[p] = v
				)?
				( ',' PARAM/<p,v>	$ s[p] = v
				)*
			'\)'
	;

	MEMBERS/l ->			$ l = {"hosts": [], "lists": []}
			( hitem/i	$ l["hosts"].append(i[1:])
			| litem/i	$ l["lists"].append(i[1:])
			| COMMENT
			)*
	;
        """ % { "keywords": "|".join(PARAMS.keys()) }

class ConfigGroup(UserDict):
	"store group info"

	def __init__(self):
		UserDict.__init__(self)
		self["name"] = ""
		# create keys that are also available globally, but with default values stripped
		self.update(dict(map(lambda x: (x,""), PARAMS.keys())))
		self["hosts"] = []
		self["lists"] = []
	
	def __str__(self):
		l = []
		for param in PARAMS.keys():
			if self[param]:
				l.append('%s="%s"' % (param, re.sub('"', '""', self[param])))
		return "group %s (%s)" % (self["name"], ', '.join(l))

class ConfigBase(UserDict):
	"store all configuration parameters"

	def __init__(self):
		UserDict.__init__(self)
		self.clear()
	
	def clear(self):
		"make configuration empty"
		self["groups"] = {}
		self["settings"] = PARAMS

	def parse(self, txt):
		
		tp = TConf()
		self.update(tp(txt))
	
	def load(self, filename):
		"load configuration from file"
		
		try:
			f = open(filename)
			try:
				self.parse("".join(f.readlines()))
			except tpg.SyntacticError, excerr:
				warn("in %s: %s" % (filename, excerr.msg))
		except IOError:
			err("could not read file: '%s'" % filename)
		f.close()
	
	def dump(self, filename):
		"save configuration to file"
		
		comment = [
		"#\n",
		"# CURRENT CONFIGURATION\n",
		"#\n",
		"# You can change the configuration for the current session here.\n",
		"# Those changes will be lost after you quit tentakel.\n",
		"# No configuration file will be changed.\n",
		"#\n"
		]

		try:
			f = open(filename, "w")
			f.writelines(comment)
			f.writelines(str(self))
		except IOError:
			err("could not write file: '%s'" % filename)
		f.close()
	
	def edit(self):
		"interactively edit configuration"

		# TODO: For now, I don't consider this a security hole
		# (symlink attack etc.) because the file is opened in an
		# editor and the user can see it.
		# When Python 2.3 becomes more standard the new tempfile
		# methods are going to be used.
		try:
			tempedit = tempfile.mktemp()
			self.dump(tempedit)
			editor = os.getenv("VISUAL") or os.getenv("EDITOR") or "vi"
			os.spawnvp(os.P_WAIT, editor, [editor, tempedit])
			self.load(tempedit)
		finally:
			os.remove(tempedit)

	def __str__(self):
		"pretty print configuration"

		out = ""
		settings = self["settings"]
		for s_param, s_value in settings.items():
			if s_value:
				out = "%sset %s=\"%s\"\n" % (out, s_param, s_value)
		out = out + "\n"
		groups = self["groups"]
		for groupName, groupObj in groups.items():
			out = out + str(groupObj) + "\n"
			for list in groups[groupName]["lists"]:
				out = out + "\t@" + list + "\n"
			for host in groups[groupName]["hosts"]:
				out = out + "\t+" + host + "\n"
			out = out + "\n"
		return out

	def getGroup(self, groupName):
		"Return the group groupName"

		return self["groups"][groupName]
	
	def getGroups(self):
		"Return list of group names"

		return self["groups"].keys()
	
	def getExpandedGroup(self, groupName):
		"Return list of groupName members with sub lists expanded recursively"

		g = self.getGroup(groupName)
		out = g["hosts"]
		for list in g["lists"]:
			try:
				out = out + self.getExpandedGroup(list)
			except (KeyError, RuntimeError):
				if sys.exc_type == KeyError:
					warn("in group '%s': no such group '%s'" % (groupName, list))
				if sys.exc_type == RuntimeError:
					err("runtime error: possible loop in configuration file")
		return out
	
	def getParam(self, param, group=None):
		"""Return the value for param
	
		If group is specified, return the groups local value for param.
		If the group has no local value or group=None or group does not
		exist, return the global value for param.
		
		If param is not a valid parameter identifier, return None"""

		if param not in PARAMS.keys():
			warn("invalid parameter: '%s'" % param)
			return None
		else:

			try:
				val = self.getGroup(group)[param]
				if val == '': return self["settings"][param]
				else: return val
			except KeyError:
				return self["settings"][param]

if __name__ == "__main__":
	
	failures = 0
	print "self testing..."

	print "### instantiate ConfigBase:"
	c1 = ConfigBase()
	if isinstance(c1, ConfigBase):
		print "OK"
	else: failures += 1

	print "### load example config:"
	try:
		c1.load("../../tentakel.conf.example")
		print "OK"
	except:
		print "-> failed <-"
		failures += 1
	
	print "### regenerate ourself from a dump:"
	tmp = tempfile.mktemp()
	c1.dump(tmp)
	c2 = ConfigBase()
	c2.load(tmp)
	if c1 == c2:
		print "OK"
	else:
		print "-> failed <-"
		failures += 1
	del c2
	os.remove(tmp)

	print "### ugly config syntax:"
	uglyconfig = [
	'# all of these should work:\n',
	'set method="ssh"\n',
	'group t1() #comment\n',
	'#comment\n',
	'group t2(format="#""") @t1 #comment\n',
	'group t3 () +local-host\n',
	'#comment\n'
	]
	tmp = tempfile.mktemp()
	tmpf = open(tmp, "w")
	tmpf.writelines(uglyconfig)
	tmpf.close()
	c3 = ConfigBase()
	try:
		c3.load(tmp)
		print "OK"
	except:
		print "-> failed <-"
		failures += 1
	del c3
	os.remove(tmp)

	print "### read parameter:"
	user1 = pwd.getpwuid(os.geteuid())[0]
	user2 = c1.getParam("user")
	if user1 == user2:
		print "OK"
	else:
		print "-> failed <-"
		print "read", user2, "but should be", user1
		failures += 1
	
	if failures:
		print "self test: encountered", failures, "failures."
		sys.exit(1)
	else:
		print "self test: no failures."
		sys.exit(0)
