summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdrian Likins <alikins@grimlock.devel.redhat.com>2008-02-25 13:28:32 -0500
committerAdrian Likins <alikins@grimlock.devel.redhat.com>2008-02-25 13:28:32 -0500
commit71ca4184404df52dabfea318e3e0a1ca8c1b4c61 (patch)
tree5a4b69c9b9e38057256624f1c173f59d928487e5
parente648895b13205a4669ba9d3ea8756209b6a6d9eb (diff)
parente27f9d8383848d523d301706046c54e99b5f9676 (diff)
downloadthird_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
-rw-r--r--func.spec4
-rw-r--r--func/commonconfig.py2
-rw-r--r--func/forkbomb.py2
-rw-r--r--func/jobthing.py2
-rw-r--r--func/minion/modules/certmaster.py4
-rw-r--r--func/minion/modules/jobs.py1
-rw-r--r--func/minion/modules/process.py66
-rwxr-xr-xfunc/minion/server.py1
-rwxr-xr-xfunc/minion/utils.py1
-rwxr-xr-xfunc/overlord/client.py7
-rw-r--r--func/overlord/cmd_modules/check.py143
-rw-r--r--func/overlord/cmd_modules/listminions.py16
-rw-r--r--func/overlord/cmd_modules/ping.py6
-rw-r--r--func/overlord/func_command.py10
-rw-r--r--func/overlord/groups.py1
-rwxr-xr-xfunc/utils.py1
-rw-r--r--funcweb/README31
-rw-r--r--funcweb/README.txt32
-rw-r--r--funcweb/dev.cfg2
-rw-r--r--funcweb/funcweb/config/app.cfg22
-rw-r--r--funcweb/funcweb/controllers.py9
-rw-r--r--funcweb/funcweb/identity/__init__.py0
-rw-r--r--funcweb/funcweb/identity/pam.py127
-rw-r--r--funcweb/funcweb/identity/pamprovider.py105
-rw-r--r--funcweb/funcweb/identity/visit.py51
-rw-r--r--funcweb/funcweb/model.py99
-rw-r--r--funcweb/setup.py8
-rwxr-xr-xscripts/func-create-module70
28 files changed, 596 insertions, 227 deletions
diff --git a/func.spec b/func.spec
index 0a05f71..c43da30 100644
--- a/func.spec
+++ b/func.spec
@@ -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 ..."