diff options
author | Adrian Likins <alikins@grimlock.devel.redhat.com> | 2008-02-25 13:28:32 -0500 |
---|---|---|
committer | Adrian Likins <alikins@grimlock.devel.redhat.com> | 2008-02-25 13:28:32 -0500 |
commit | 71ca4184404df52dabfea318e3e0a1ca8c1b4c61 (patch) | |
tree | 5a4b69c9b9e38057256624f1c173f59d928487e5 | |
parent | e648895b13205a4669ba9d3ea8756209b6a6d9eb (diff) | |
parent | e27f9d8383848d523d301706046c54e99b5f9676 (diff) | |
download | third_party-func-71ca4184404df52dabfea318e3e0a1ca8c1b4c61.tar.gz third_party-func-71ca4184404df52dabfea318e3e0a1ca8c1b4c61.tar.xz third_party-func-71ca4184404df52dabfea318e3e0a1ca8c1b4c61.zip |
Merge branch 'master' of ssh://git.fedoraproject.org/git/hosted/func
28 files changed, 596 insertions, 227 deletions
@@ -3,7 +3,7 @@ %define is_suse %(test -e /etc/SuSE-release && echo 1 || echo 0) -Summary: Remote config, monitoring, and management api +Summary: Remote management framework Name: func Source1: version Version: %(echo `awk '{ print $1 }' %{SOURCE1}`) @@ -30,7 +30,7 @@ Url: https://hosted.fedoraproject.org/projects/func/ %description -func is a remote api for mangement, configation, and monitoring of systems. +func is a remote api for mangement, configuration, and monitoring of systems. %prep %setup -q diff --git a/func/commonconfig.py b/func/commonconfig.py index 9b4612e..292eb45 100644 --- a/func/commonconfig.py +++ b/func/commonconfig.py @@ -1,4 +1,4 @@ -from config import BaseConfig, BoolOption, IntOption, Option +from config import BaseConfig, BoolOption, Option class CMConfig(BaseConfig): listen_addr = Option('') diff --git a/func/forkbomb.py b/func/forkbomb.py index 3dfa6f2..73fa924 100644 --- a/func/forkbomb.py +++ b/func/forkbomb.py @@ -20,8 +20,6 @@ import bsddb import sys import tempfile import fcntl -import utils -import xmlrpclib DEFAULT_FORKS = 4 DEFAULT_CACHE_DIR = "/var/lib/func" diff --git a/func/jobthing.py b/func/jobthing.py index 67ad1a6..75a1d1a 100644 --- a/func/jobthing.py +++ b/func/jobthing.py @@ -20,11 +20,9 @@ import time # for testing only import shelve import bsddb import sys -import tempfile import fcntl import forkbomb import utils -import traceback JOB_ID_RUNNING = 0 JOB_ID_FINISHED = 1 diff --git a/func/minion/modules/certmaster.py b/func/minion/modules/certmaster.py index 9ca484f..c30a39c 100644 --- a/func/minion/modules/certmaster.py +++ b/func/minion/modules/certmaster.py @@ -13,10 +13,6 @@ ## Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. ## -# other modules -import sub_process -import codes - # our modules import func_module from func import certmaster as certmaster diff --git a/func/minion/modules/jobs.py b/func/minion/modules/jobs.py index 69fb75f..90c7421 100644 --- a/func/minion/modules/jobs.py +++ b/func/minion/modules/jobs.py @@ -14,7 +14,6 @@ ## Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. ## -import codes from func import jobthing import func_module diff --git a/func/minion/modules/process.py b/func/minion/modules/process.py index 848e847..22141c3 100644 --- a/func/minion/modules/process.py +++ b/func/minion/modules/process.py @@ -72,7 +72,7 @@ class ProcessModule(func_module.FuncModule): import os our_pid=os.getpid() results = [] - have_smaps=0 + global have_pss have_pss=0 def kernel_ver(): @@ -85,79 +85,83 @@ class ProcessModule(func_module.FuncModule): kv=kernel_ver() def getMemStats(pid): - """ return Rss,Pss,Shared (note Private = Rss-Shared) """ + """ return Private,Shared """ + global have_pss + Private_lines=[] Shared_lines=[] Pss_lines=[] pagesize=os.sysconf("SC_PAGE_SIZE")/1024 #KiB Rss=int(open("/proc/"+str(pid)+"/statm").readline().split()[1])*pagesize if os.path.exists("/proc/"+str(pid)+"/smaps"): #stat - global have_smaps - have_smaps=1 for line in open("/proc/"+str(pid)+"/smaps").readlines(): #open - #Note in smaps Shared+Private = Rss above - #The Rss in smaps includes video card mem etc. if line.startswith("Shared"): Shared_lines.append(line) + elif line.startswith("Private"): + Private_lines.append(line) elif line.startswith("Pss"): - global have_pss have_pss=1 Pss_lines.append(line) Shared=sum([int(line.split()[1]) for line in Shared_lines]) - Pss=sum([int(line.split()[1]) for line in Pss_lines]) + Private=sum([int(line.split()[1]) for line in Private_lines]) + #Note Shared + Private = Rss above + #The Rss in smaps includes video card mem etc. + if have_pss: + pss_adjust=0.5 #add 0.5KiB as this average error due to trunctation + Pss=sum([float(line.split()[1])+pss_adjust for line in Pss_lines]) + Shared = Pss - Private elif (2,6,1) <= kv <= (2,6,9): - Pss=0 Shared=0 #lots of overestimation, but what can we do? + Private = Rss else: - Pss=0 Shared=int(open("/proc/"+str(pid)+"/statm").readline().split()[2])*pagesize - return (Rss, Pss, Shared) + Private = Rss - Shared + return (Private, Shared) + + def getCmdName(pid): + cmd = file("/proc/%d/status" % pid).readline()[6:-1] + exe = os.path.basename(os.path.realpath("/proc/%d/exe" % pid)) + if exe.startswith(cmd): + cmd=exe #show non truncated version + #Note because we show the non truncated name + #one can have separated programs as follows: + #584.0 KiB + 1.0 MiB = 1.6 MiB mozilla-thunder (exe -> bash) + # 56.0 MiB + 22.2 MiB = 78.2 MiB mozilla-thunderbird-bin + return cmd cmds={} shareds={} count={} for pid in os.listdir("/proc/"): try: - pid = int(pid) #note Thread IDs not listed in /proc/ - if pid ==our_pid: continue + pid = int(pid) #note Thread IDs not listed in /proc/ which is good + if pid == our_pid: continue except: continue - cmd = file("/proc/%d/status" % pid).readline()[6:-1] try: - exe = os.path.basename(os.path.realpath("/proc/%d/exe" % pid)) - if exe.startswith(cmd): - cmd=exe #show non truncated version - #Note because we show the non truncated name - #one can have separated programs as follows: - #584.0 KiB + 1.0 MiB = 1.6 MiB mozilla-thunder (exe -> bash) - #56.0 MiB + 22.2 MiB = 78.2 MiB mozilla-thunderbird-bin + cmd = getCmdName(pid) except: #permission denied or #kernel threads don't have exe links or #process gone continue try: - rss, pss, shared = getMemStats(pid) - private = rss-shared - #Note shared is always a subset of rss (trs is not always) + private, shared = getMemStats(pid) except: continue #process gone if shareds.get(cmd): - if pss: #add shared portion of PSS together - shareds[cmd]+=pss-private + if have_pss: #add shared portion of PSS together + shareds[cmd]+=shared elif shareds[cmd] < shared: #just take largest shared val shareds[cmd]=shared else: - if pss: - shareds[cmd]=pss-private - else: - shareds[cmd]=shared + shareds[cmd]=shared cmds[cmd]=cmds.setdefault(cmd,0)+private if count.has_key(cmd): count[cmd] += 1 else: count[cmd] = 1 - #Add max shared mem for each program + #Add shared mem for each program total=0 for cmd in cmds.keys(): cmds[cmd]=cmds[cmd]+shareds[cmd] diff --git a/func/minion/server.py b/func/minion/server.py index f1b827f..2fa175a 100755 --- a/func/minion/server.py +++ b/func/minion/server.py @@ -17,7 +17,6 @@ import SimpleXMLRPCServer import string import sys import traceback -import socket import fnmatch from gettext import textdomain diff --git a/func/minion/utils.py b/func/minion/utils.py index a7ea788..ea8854c 100755 --- a/func/minion/utils.py +++ b/func/minion/utils.py @@ -18,7 +18,6 @@ import time import traceback import xmlrpclib import glob -import traceback import codes from func import certs diff --git a/func/overlord/client.py b/func/overlord/client.py index f07e526..fdcf875 100755 --- a/func/overlord/client.py +++ b/func/overlord/client.py @@ -105,6 +105,7 @@ class Minions(object): def _get_new_hosts(self): self.new_hosts = self.group_class.get_hosts_by_groupgoo(self.spec) + return self.new_hosts def _get_all_hosts(self): seperate_gloobs = self.spec.split(";") @@ -116,6 +117,12 @@ class Minions(object): self.all_certs.append(cert) host = cert.replace(self.config.certroot,"")[1:-5] self.all_hosts.append(host) + return self.all_hosts + + def get_all_hosts(self): + self._get_new_hosts() + self._get_all_hosts() + return self.all_hosts def get_urls(self): self._get_new_hosts() diff --git a/func/overlord/cmd_modules/check.py b/func/overlord/cmd_modules/check.py new file mode 100644 index 0000000..cf1badb --- /dev/null +++ b/func/overlord/cmd_modules/check.py @@ -0,0 +1,143 @@ +""" +check checks to see how happy func is. +it provides sanity checks for basic user setup. + +Copyright 2008, Red Hat, Inc +Michael DeHaan <mdehaan@redhat.com> + +This software may be freely redistributed under the terms of the GNU +general public license. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +""" + + +import optparse +import os +import urllib2 + +from func.overlord import command +from func.overlord import client +from func import utils +from func.minion import sub_process +from func.config import read_config +from func.commonconfig import FuncdConfig + +# FIXME: don't hardcode this here +DEFAULT_PORT = 51234 + +class CheckAction(client.command.Command): + name = "check" + usage = "check func for possible setup problems" + + def addOptions(self): + self.parser.add_option("-c", "--certmaster", action="store_true", help="check the certmaster configuration on this box") + self.parser.add_option("-m", "--minion", action="store_true", help="check the minion configuration on this box") + + + def handleOptions(self, options): + # FIXME: all through the code we have this constant in each + # file, need to make this common. + self.port = DEFAULT_PORT + self.check_certmaster = options.certmaster + self.check_minion = options.minion + + def do(self, args): + + if not self.check_certmaster and not self.check_minion: + print "* specify --certmaster, --minion, or both" + return + else: + print "SCAN RESULTS:" + + hostname = utils.get_hostname() + print "* FQDN is detected as %s, verify that is correct" % hostname + self.check_iptables() + + if not os.getuid() == 0: + print "* root is required to run these setup tests" + return + + if self.check_minion: + + # check that funcd is running + self.check_service("funcd") + + # check that the configured certmaster is reachable + self.check_talk_to_certmaster() + + if self.check_certmaster: + + # check that certmasterd is running + self.check_service("certmasterd") + + # see if we have any waiting CSRs + # FIXME: TODO + + # see if we have signed any certs + # FIXME: TODO + + # construct a client handle and see if any hosts are reachable + self.server_spec = self.parentCommand.server_spec + + client_obj = client.Client( + self.server_spec, + port=self.port, + interactive=False, + verbose=False, + config=self.config + ) + results = client_obj.test.add(1,2) + hosts = results.keys() + if len(hosts) == 0: + print "* no systems have signed certs" + else: + failed = 0 + for x in hosts: + if results[x] != 3: + failed = failed+1 + if failed != 0: + print "* unable to connect to %s registered minions from overlord" % failed + print "* run func '*' ping to check status" + + # see if any of our certs have expired + + # warn about iptables if running + print "End of Report." + + def check_service(self, which): + if os.path.exists("/etc/rc.d/init.d/%s" % which): + rc = sub_process.call("/sbin/service %s status >/dev/null 2>/dev/null" % which, shell=True) + if rc != 0: + print "* service %s is not running" % which + + def check_iptables(self): + if os.path.exists("/etc/rc.d/init.d/iptables"): + rc = sub_process.call("/sbin/service iptables status >/dev/null 2>/dev/null", shell=True) + + if rc == 0: + # FIXME: don't hardcode port + print "* iptables may be running, ensure 51234 is unblocked" + + def check_talk_to_certmaster(self): + config_file = '/etc/func/minion.conf' + config = read_config(config_file, FuncdConfig) + cert_dir = config.cert_dir + # FIXME: don't hardcode port + master_uri = "http://%s:51235/" % config.certmaster + print "* this minion is configured in /etc/func/minion.conf to talk to host '%s' for certs, verify that is correct" % config.certmaster + # this will be a 501, unsupported GET, but we should be + # able to tell if we can make contact + connect_ok = True + try: + fd = urllib2.urlopen(master_uri) + data = fd.read() + fd.close() + except urllib2.HTTPError: + pass + except: + connect_ok = False + if not connect_ok: + print "cannot connect to certmaster at %s" % (master_uri) diff --git a/func/overlord/cmd_modules/listminions.py b/func/overlord/cmd_modules/listminions.py index 50c7e24..9421b8d 100644 --- a/func/overlord/cmd_modules/listminions.py +++ b/func/overlord/cmd_modules/listminions.py @@ -1,5 +1,6 @@ """ -copyfile command line +list minions provides a command line way to see what certs are +registered. Copyright 2007, Red Hat, Inc see AUTHORS @@ -42,10 +43,13 @@ class ListMinions(client.command.Command): verbose=self.options.verbose, config=self.config) - servers = client_obj.servers - print servers + results = client_obj.test.add(1,2) + servers = results.keys() + servers.sort() + + # print servers for server in servers: # just cause I hate regex'es -akl - host = server.split(':')[-2] - host = host.split('/')[-1] - print host + # host = server.split(':')[-2] + # host = host.split('/')[-1] + print server diff --git a/func/overlord/cmd_modules/ping.py b/func/overlord/cmd_modules/ping.py index f756fd9..438e2a9 100644 --- a/func/overlord/cmd_modules/ping.py +++ b/func/overlord/cmd_modules/ping.py @@ -1,5 +1,5 @@ """ -copyfile command line +ping minions to see whether they are up. Copyright 2007, Red Hat, Inc Michael DeHaan <mdehaan@redhat.com> @@ -52,8 +52,8 @@ class Ping(client.command.Command): # because this is mainly an interactive command, expand the server list and make seperate connections. # to make things look more speedy. - servers = client.expand_servers(self.server_spec, port=self.options.port, noglobs=None, - verbose=self.options.verbose, just_fqdns=True) + minion_set = client.Minions(self.server_spec, port=self.options.port) + servers = minion_set.get_all_hosts() for server in servers: diff --git a/func/overlord/func_command.py b/func/overlord/func_command.py index 8bc6b7c..bd718bb 100644 --- a/func/overlord/func_command.py +++ b/func/overlord/func_command.py @@ -23,15 +23,19 @@ from cmd_modules import show from cmd_modules import copyfile from cmd_modules import listminions from cmd_modules import ping +from cmd_modules import check from func.overlord import client class FuncCommandLine(command.Command): + name = "func" - usage = "func is the commandline interface to a func minion" + usage = "func is the command line interface for controlling func minions" - subCommandClasses = [call.Call, show.Show, - copyfile.CopyFile, listminions.ListMinions, ping.Ping] + subCommandClasses = [ + call.Call, show.Show, copyfile.CopyFile, + listminions.ListMinions, ping.Ping, check.CheckAction + ] def __init__(self): diff --git a/func/overlord/groups.py b/func/overlord/groups.py index a0a9d78..7097366 100644 --- a/func/overlord/groups.py +++ b/func/overlord/groups.py @@ -24,7 +24,6 @@ import ConfigParser -import os class Groups(object): diff --git a/func/utils.py b/func/utils.py index 4aca97e..9577bd9 100755 --- a/func/utils.py +++ b/func/utils.py @@ -14,7 +14,6 @@ import os import string import sys import traceback -import xmlrpclib import socket REMOTE_ERROR = "REMOTE_ERROR" diff --git a/funcweb/README b/funcweb/README new file mode 100644 index 0000000..5acab13 --- /dev/null +++ b/funcweb/README @@ -0,0 +1,31 @@ +funcweb +======= + +A TurboGears interface to func. + +This project is currently under development, and is currently just a +proof-of-concept and should not be used in a production environment. + +Running +======= + + 1) Setup func. https://fedorahosted.org/func/wiki/InstallAndSetupGuide + Be sure to setup a non-root user to run the func client, so you don't have + to run funcweb as root. + + 2) Install the necessary software + + # yum install TurboGears python-genshi + + 3) Setup and run funcweb + + $ python setup.py egg_info + $ ./start-funcweb.py + + 4) Use funcweb + + Connect to http://localhost:8080 + +Authors +======= +Luke Macken <lmacken@redhat.com> diff --git a/funcweb/README.txt b/funcweb/README.txt deleted file mode 100644 index 4c52dcd..0000000 --- a/funcweb/README.txt +++ /dev/null @@ -1,32 +0,0 @@ -funcweb -======= - -A TurboGears interface to func. - -This project is currently under development, and is currently just a -proof-of-concept and should not be used in a production environment. - -Running -======= - - # yum install TurboGears python-genshi python-elixir - $ python setup.py egg_info - $ tg-admin sql create - # ./start-funcweb.py - -Connect to http://localhost:8080 - -Creating a new user -=================== - -Currently funcweb only allows connections from 127.0.0.1 and from authenticated -users. So if you wish to grant other people access to your funcweb instance, -you can create new users easily: - - $ tg-admin shell - >>> user = User(user_name='name', password='password') - >>> ^D - -Authors -======= -Luke Macken <lmacken@redhat.com> diff --git a/funcweb/dev.cfg b/funcweb/dev.cfg index 638c92b..d1c48b6 100644 --- a/funcweb/dev.cfg +++ b/funcweb/dev.cfg @@ -15,7 +15,7 @@ # If you have sqlite, here's a simple default to get you started # in development -sqlalchemy.dburi="sqlite:///devdata.sqlite" +# sqlalchemy.dburi="sqlite:///devdata.sqlite" # SERVER diff --git a/funcweb/funcweb/config/app.cfg b/funcweb/funcweb/config/app.cfg index 504f018..e691bde 100644 --- a/funcweb/funcweb/config/app.cfg +++ b/funcweb/funcweb/config/app.cfg @@ -19,12 +19,6 @@ tg.defaultview = "genshi" # Allow every exposed function to be called as json, # tg.allow_json = False -# Suppress the inclusion of the shipped MochiKit version, which is rather outdated. -# Attention: setting this to True and listing 'turbogears.mochikit' in 'tg.include_widgets' -# is a contradiction. This option will overrule the default-inclusion to prevent version -# mismatch bugs. -# tg.mochikit_suppress = True - # List of Widgets to include on every page. # for example ['turbogears.mochikit'] # tg.include_widgets = [] @@ -67,11 +61,13 @@ visit.on=True # visit.cookie.path="/" # The name of the VisitManager plugin to use for visitor tracking. -visit.manager="sqlalchemy" +#visit.manager="sqlalchemy" +visit.manager="funcvisit" # Database class to use for visit tracking -visit.saprovider.model = "funcweb.model.Visit" -identity.saprovider.model.visit = "funcweb.model.VisitIdentity" +#visit.saprovider.model = "funcweb.identity.model.Visit" +#identity.saprovider.model.visit = "funcweb.identity.model.VisitIdentity" + # IDENTITY # General configuration of the TurboGears Identity management module @@ -85,7 +81,7 @@ identity.on=True # option must be specified. identity.failure_url="/login" -identity.provider='sqlalchemy' +identity.provider='pam' # The names of the fields on the login form containing the visitor's user ID # and password. In addition, the submit button is specified simply so its @@ -107,9 +103,9 @@ identity.provider='sqlalchemy' # The classes you wish to use for your Identity model. Remember to not use reserved # SQL keywords for class names (at least unless you specify a different table # name using sqlmeta). -identity.saprovider.model.user="funcweb.model.User" -identity.saprovider.model.group="funcweb.model.Group" -identity.saprovider.model.permission="funcweb.model.Permission" +#identity.saprovider.model.user="funcweb.identity.model.User" +#identity.saprovider.model.group="funcweb.identity.model.Group" +#identity.saprovider.model.permission="funcweb.identity.model.Permission" # The password encryption algorithm used when comparing passwords against what's # stored in the database. Valid values are 'md5' or 'sha1'. If you do not diff --git a/funcweb/funcweb/controllers.py b/funcweb/funcweb/controllers.py index df4c05c..c08844f 100644 --- a/funcweb/funcweb/controllers.py +++ b/funcweb/funcweb/controllers.py @@ -7,8 +7,7 @@ from func.overlord.client import Client class Root(controllers.RootController): @expose(template="funcweb.templates.minions") - @identity.require(identity.Any( - identity.from_host("127.0.0.1"), identity.not_anonymous())) + @identity.require(identity.not_anonymous()) def minions(self, glob='*'): """ Return a list of our minions that match a given glob """ fc = Client(glob) @@ -17,8 +16,7 @@ class Root(controllers.RootController): index = minions # start with our minion view, for now @expose(template="funcweb.templates.minion") - @identity.require(identity.Any( - identity.from_host("127.0.0.1"), identity.not_anonymous())) + @identity.require(identity.not_anonymous()) def minion(self, name, module=None, method=None): """ Display module or method details for a specific minion. @@ -42,8 +40,7 @@ class Root(controllers.RootController): @expose(template="funcweb.templates.run") - @identity.require(identity.Any( - identity.from_host("127.0.0.1"), identity.not_anonymous())) + @identity.require(identity.not_anonymous()) def run(self, minion="*", module=None, method=None, arguments=''): fc = Client(minion) results = getattr(getattr(fc, module), method)(*arguments.split()) diff --git a/funcweb/funcweb/identity/__init__.py b/funcweb/funcweb/identity/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/funcweb/funcweb/identity/__init__.py diff --git a/funcweb/funcweb/identity/pam.py b/funcweb/funcweb/identity/pam.py new file mode 100644 index 0000000..aed5420 --- /dev/null +++ b/funcweb/funcweb/identity/pam.py @@ -0,0 +1,127 @@ +# (c) 2007 Chris AtLee <chris@atlee.ca> +# Licensed under the MIT license: +# http://www.opensource.org/licenses/mit-license.php +""" +PAM module for python + +Provides an authenticate function that will allow the caller to authenticate +a user against the Pluggable Authentication Modules (PAM) on the system. + +Implemented using ctypes, so no compilation is necessary. +""" +__all__ = ['authenticate'] + +from ctypes import CDLL, POINTER, Structure, CFUNCTYPE, cast, pointer, sizeof +from ctypes import c_void_p, c_uint, c_char_p, c_char, c_int + + +LIBPAM = CDLL("libpam.so") +LIBC = CDLL("libc.so.6") + +CALLOC = LIBC.calloc +CALLOC.restype = c_void_p +CALLOC.argtypes = [c_uint, c_uint] + +STRDUP = LIBC.strdup +STRDUP.argstypes = [c_char_p] +STRDUP.restype = POINTER(c_char) # NOT c_char_p !!!! + +# Various constants +PAM_PROMPT_ECHO_OFF = 1 +PAM_PROMPT_ECHO_ON = 2 +PAM_ERROR_MSG = 3 +PAM_TEXT_INFO = 4 + +class PamHandle(Structure): + """wrapper class for pam_handle_t""" + _fields_ = [ + ("handle", c_void_p) + ] + + def __init__(self): + Structure.__init__(self) + self.handle = 0 + +class PamMessage(Structure): + """wrapper class for pam_message structure""" + _fields_ = [ + ("msg_style", c_int), + ("msg", c_char_p), + ] + + def __repr__(self): + return "<PamMessage %i '%s'>" % (self.msg_style, self.msg) + +class PamResponse(Structure): + """wrapper class for pam_response structure""" + _fields_ = [ + ("resp", c_char_p), + ("resp_retcode", c_int), + ] + + def __repr__(self): + return "<PamResponse %i '%s'>" % (self.resp_retcode, self.resp) + +CONV_FUNC = CFUNCTYPE(c_int, + c_int, POINTER(POINTER(PamMessage)), + POINTER(POINTER(PamResponse)), c_void_p) + +class PamConv(Structure): + """wrapper class for pam_conv structure""" + _fields_ = [ + ("conv", CONV_FUNC), + ("appdata_ptr", c_void_p) + ] + +PAM_START = LIBPAM.pam_start +PAM_START.restype = c_int +PAM_START.argtypes = [c_char_p, c_char_p, POINTER(PamConv), + POINTER(PamHandle)] + +PAM_AUTHENTICATE = LIBPAM.pam_authenticate +PAM_AUTHENTICATE.restype = c_int +PAM_AUTHENTICATE.argtypes = [PamHandle, c_int] + +def authenticate(username, password, service='login'): + """Returns True if the given username and password authenticate for the + given service. Returns False otherwise + + ``username``: the username to authenticate + + ``password``: the password in plain text + + ``service``: the PAM service to authenticate against. + Defaults to 'login'""" + @CONV_FUNC + def my_conv(n_messages, messages, p_response, app_data): + """Simple conversation function that responds to any + prompt where the echo is off with the supplied password""" + # Create an array of n_messages response objects + addr = CALLOC(n_messages, sizeof(PamResponse)) + p_response[0] = cast(addr, POINTER(PamResponse)) + for i in range(n_messages): + if messages[i].contents.msg_style == PAM_PROMPT_ECHO_OFF: + pw_copy = STRDUP(password) + p_response.contents[i].resp = cast(pw_copy, c_char_p) + p_response.contents[i].resp_retcode = 0 + return 0 + + # STRDUP expects byte strings + if isinstance(password, unicode): + password = str(password) + + handle = PamHandle() + conv = PamConv(my_conv, 0) + retval = PAM_START(service, username, pointer(conv), pointer(handle)) + + if retval != 0: + # TODO: This is not an authentication error, something + # has gone wrong starting up PAM + return False + + retval = PAM_AUTHENTICATE(handle, 0) + return retval == 0 + +if __name__ == "__main__": + import getpass + print authenticate(getpass.getuser(), getpass.getpass()) diff --git a/funcweb/funcweb/identity/pamprovider.py b/funcweb/funcweb/identity/pamprovider.py new file mode 100644 index 0000000..68aedfb --- /dev/null +++ b/funcweb/funcweb/identity/pamprovider.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# +# Copyright © 2008 Red Hat, Inc. All rights reserved. +# +# This copyrighted material is made available to anyone wishing to use, modify, +# copy, or redistribute it subject to the terms and conditions of the GNU +# General Public License v.2. This program is distributed in the hope that it +# will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the +# implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU General Public License for more details. You should have +# received a copy of the GNU General Public License along with this program; if +# not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth +# Floor, Boston, MA 02110-1301, USA. Any Red Hat trademarks that are +# incorporated in the source code or documentation are not subject to the GNU +# General Public License and may only be used or replicated with the express +# permission of Red Hat, Inc. +# +# Author(s): Luke Macken <lmacken@redhat.com> + +""" +This module contains an Identity Provider used by TurboGears to authenticate +users against PAM. It utilizes the pam.py module written by Chris AtLee. +http://pypi.python.org/pypi/pam/0.1.2 + +To utilize, simply define the following in your app.cfg: + + identity.provider = 'pam' +""" + +import pam +import logging + +from turbogears import identity + +log = logging.getLogger(__name__) + +class User(object): + def __init__(self, username): + self.user_id = username + self.user_name = username + self.display_name = username + +class Identity: + + def __init__(self, visit_key=None, username=None): + self.username = username + self.visit_key = visit_key + self.expired = False + + def _get_user(self): + try: + return self._user + except AttributeError: + return None + if not self.visit_key: + self._user = None + return None + self._user = User(self.username) + return self._user + user = property(_get_user) + + def _get_anonymous(self): + return not self.username + anonymous = property(_get_anonymous) + + + def logout(self): + if not self.visit_key: + return + self.expired = True + anon = Identity(None,None) + identity.set_current_identity(anon) + + +class PAMIdentityProvider: + """ + IdentityProvider that authenticates users against PAM. + """ + users = {} + + def validate_identity(self, user_name, password, visit_key): + if not self.validate_password(user_name, password): + log.warning("Invalid password for %s" % user_name) + return None + log.info("Login successful for %s" % user_name) + user = Identity(visit_key, user_name) + self.users[visit_key] = user + return user + + def validate_password(self,user_name, password): + return pam.authenticate(user_name, password) + + def load_identity(self, visit_key): + if self.users.has_key(visit_key): + if self.users[visit_key].expired: + del self.users[visit_key] + return None + return self.users[visit_key] + return None + + def anonymous_identity(self): + return Identity(None) + + def create_provider_model(self): + pass diff --git a/funcweb/funcweb/identity/visit.py b/funcweb/funcweb/identity/visit.py new file mode 100644 index 0000000..be3879e --- /dev/null +++ b/funcweb/funcweb/identity/visit.py @@ -0,0 +1,51 @@ +from datetime import datetime + +from sqlalchemy import * +from sqlalchemy.orm import class_mapper + +from turbogears import config +from turbogears.util import load_class +from turbogears.visit.api import BaseVisitManager, Visit +from turbogears.database import get_engine, metadata, session, mapper + +import logging +log = logging.getLogger(__name__) + + +class FuncWebVisitManager(BaseVisitManager): + + def __init__(self, timeout): + super(FuncWebVisitManager,self).__init__(timeout) + self.visits = {} + + def create_model(self): + pass + + def new_visit_with_key(self, visit_key): + log.debug("new_visit_with_key(%s)" % visit_key) + created = datetime.now() + visit = Visit(visit_key, True) + visit.visit_key = visit_key + visit.created = created + visit.expiry = created + self.timeout + self.visits[visit_key] = visit + log.debug("returning %s" % visit) + return visit + + def visit_for_key(self, visit_key): + ''' + Return the visit for this key or None if the visit doesn't exist or has + expired. + ''' + log.debug("visit_for_key(%s)" % visit_key) + if not self.visits.has_key(visit_key): + return None + visit = self.visits[visit_key] + if not visit: + return None + now = datetime.now(visit.expiry.tzinfo) + if visit.expiry < now: + return None + visit.is_new = False + log.debug("returning %s" % visit) + return visit diff --git a/funcweb/funcweb/model.py b/funcweb/funcweb/model.py deleted file mode 100644 index 2997cf0..0000000 --- a/funcweb/funcweb/model.py +++ /dev/null @@ -1,99 +0,0 @@ -from datetime import datetime -# import the basic Elixir classes and functions for declaring the data model -# (see http://elixir.ematia.de/trac/wiki/TutorialDivingIn) -from elixir import Entity, Field, OneToMany, ManyToOne, ManyToMany -from elixir import options_defaults, using_options, setup_all -# import some datatypes for table columns from Elixir -# (see http://www.sqlalchemy.org/docs/04/types.html for more) -from elixir import String, Unicode, Integer, DateTime -from turbogears import identity - -options_defaults['autosetup'] = False - - -# your data model - -# class YourDataClass(Entity): -# pass - - -# the identity model - - -class Visit(Entity): - """ - A visit to your site - """ - using_options(tablename='visit') - - visit_key = Field(String(40), primary_key=True) - created = Field(DateTime, nullable=False, default=datetime.now) - expiry = Field(DateTime) - - @classmethod - def lookup_visit(cls, visit_key): - return Visit.get(visit_key) - - -class VisitIdentity(Entity): - """ - A Visit that is link to a User object - """ - using_options(tablename='visit_identity') - - visit_key = Field(String(40), primary_key=True) - user = ManyToOne('User', colname='user_id', use_alter=True) - - -class Group(Entity): - """ - An ultra-simple group definition. - """ - using_options(tablename='tg_group') - - group_id = Field(Integer, primary_key=True) - group_name = Field(Unicode(16), unique=True) - display_name = Field(Unicode(255)) - created = Field(DateTime, default=datetime.now) - users = ManyToMany('User', tablename='user_group') - permissions = ManyToMany('Permission', tablename='group_permission') - - -class User(Entity): - """ - Reasonably basic User definition. - Probably would want additional attributes. - """ - using_options(tablename='tg_user') - - user_id = Field(Integer, primary_key=True) - user_name = Field(Unicode(16), unique=True) - email_address = Field(Unicode(255), unique=True) - display_name = Field(Unicode(255)) - password = Field(Unicode(40)) - created = Field(DateTime, default=datetime.now) - groups = ManyToMany('Group', tablename='user_group') - - @property - def permissions(self): - perms = set() - for g in self.groups: - perms |= set(g.permissions) - return perms - - -class Permission(Entity): - """ - A relationship that determines what each Group can do - """ - using_options(tablename='permission') - - permission_id = Field(Integer, primary_key=True) - permission_name = Field(Unicode(16), unique=True) - description = Field(Unicode(255)) - groups = ManyToMany('Group', tablename='group_permission') - - -# Set up all Elixir entities declared above - -setup_all() diff --git a/funcweb/setup.py b/funcweb/setup.py index 9bde340..c634983 100644 --- a/funcweb/setup.py +++ b/funcweb/setup.py @@ -68,6 +68,14 @@ setup( 'console_scripts': [ 'start-funcweb = funcweb.commands:start', ], + + 'turbogears.identity.provider' : [ + 'pam = funcweb.identity.pamprovider:PAMIdentityProvider' + ], + + 'turbogears.visit.manager' : [ + 'funcvisit = funcweb.identity.visit:FuncWebVisitManager' + ], }, # Uncomment next line and create a default.cfg file in your project dir # if you want to package a default configuration in your egg. diff --git a/scripts/func-create-module b/scripts/func-create-module index f2885e8..4786cb0 100755 --- a/scripts/func-create-module +++ b/scripts/func-create-module @@ -10,6 +10,10 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +""" +Creates a boilerplate minion module. +""" + TEMPLATE = """\ # @@ -25,6 +29,7 @@ TEMPLATE = """\ import func_module + class %s(func_module.FuncModule): # Update these if need be. @@ -35,6 +40,7 @@ class %s(func_module.FuncModule): %s """ + METHOD_TEMPLATE = '''\ def %s(self): """ @@ -58,22 +64,52 @@ def populate_template(author_name, author_email, module_name, desc, methods): author_email, module_name, desc, actual_methods[:-2]) -if __name__ == '__main__': - module_name = raw_input("Module Name: ").capitalize() - desc = raw_input("Description: ") - author_name = raw_input("Author: ") - author_email = raw_input("Email: ") - methods = [] - print "\nLeave blank to finish." + +def get_email(): + """ + Get and return a valid email address. + """ + import re + + regx = "^.+\\@(\\[?)[a-zA-Z0-9\\-\\.]+\\.([a-zA-Z]{2,3}|[0-9]{1,3})(\\]?)$" while True: - method = raw_input("Method: ") - if method == '': + author_email = get_input("Email") + if re.match(regx, author_email) != None: break - methods.append(method) - # Write it out to a file - file_name = "%s.py" % module_name.lower() - file_obj = open(file_name, "w") - file_obj.write(populate_template(author_name, author_email, - module_name, desc, methods)) - file_obj.close() - print "Your module is ready to be hacked on. Wrote out to %s." % file_name + print "Please enter a valid email!" + return author_email + + +def get_input(prompt): + """ + Get input and make sure input is given. + """ + result = raw_input("%s: " % prompt) + if not result: + print "Please input the requested information." + return get_input(prompt) + return result + + +if __name__ == '__main__': + try: + MODULE_NAME = get_input("Module Name").capitalize() + DESC = get_input("Description") + AUTHOR_NAME = get_input("Author") + AUTHOR_EMAIL = get_email() + METHODS = [] + print "\nLeave blank to finish." + while True: + METHOD = raw_input("Method: ") + if METHOD == '': + break + METHODS.append(METHOD) + # Write it out to a file + FILE_NAME = "%s.py" % MODULE_NAME.lower() + FILE_OBJ = open(FILE_NAME, "w") + FILE_OBJ.write(populate_template(AUTHOR_NAME, AUTHOR_EMAIL, + MODULE_NAME, DESC, METHODS)) + FILE_OBJ.close() + print "Your module is ready to be hacked on. Wrote out to %s." % FILE_NAME + except KeyboardInterrupt, ex: + print "\nExiting ..." |