diff --git a/bin/createclient b/bin/createclient deleted file mode 100755 index 807e40b..0000000 --- a/bin/createclient +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/python -# -*- coding: UTF-8 -*- -# -# Copyright (C) 2007 by Jan Dittberner. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# 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. -# -# Version: $Id$ - -import getopt, sys -from gnuviechadmin import client, exceptions - -def usage(): - print """Usage: %s [-h|--help] [-v|--verbose] - [-t |--title=<title>] - -f <firstname>|--firstname=<firstname> -l <lastname>|--lastname=<lastname> - -a <address1>|--address=<address1> [--address2=<address2>] - -z <zip>|--zip=<zip> -c <city>|--city=<city> [--country=<isocode>] - [-o <organisation>|--organisation=<organisation>] - -e <email>|--email=<email> -p <phone>|--phone=<phone> - [-m <mobile>|--mobile=<mobile>] [-x <fax>|--fax=<fax>] - -General options: - -h, --help show this usage message and exit - -v, --verbose verbose operation - -Mandatory client data options: - -f, --firstname firstname - -l, --lastname lastname - -a, --address street address - -z, --zip zip or postal code - -c, --city city or location - -e, --email contact email address - -p, --phone telephone number - -Optional client data options: - --address2 optional second line of the street address - -o, --organisation option organisation - --country country (defaults to de) - -t, --title optional title - -m, --mobile optional mobile number - -x, --fax optional fax number -""" % (sys.argv[0]) - -def main(): - try: - opts, args = getopt.gnu_getopt(sys.argv[1:], - "hvf:l:a:z:c:e:p:o:t:m:x:", - ["help", "verbose", "firstname=", - "lastname=", "address=", "zip=", - "city=", "email=", "phone=", - "address2=", "organisation=", - "country=", "title=", "mobile=", - "fax="]) - except getopt.GetoptError: - usage() - sys.exit(2) - clientdata = {} - verbose = False - for o, a in opts: - if o in ("-v", "--verbose"): - verbose = True - if o in ("-h", "--help"): - usage() - sys.exit() - if o in ("-f", "--firstname"): - clientdata["firstname"] = a - if o in ("-l", "--lastname"): - clientdata["lastname"] = a - if o in ("-a", "--address"): - clientdata["address1"] = a - if o in ("-z", "--zip"): - clientdata["zip"] = a - if o in ("-c", "--city"): - clientdata["city"] = a - if o == "--country": - clientdata["country"] = a - if o in ("-t", "--title"): - clientdata["title"] = a - if o in ("-m", "--mobile"): - clientdata["mobile"] = a - if o in ("-e", "--email"): - clientdata["email"] = a - if o in ("-o", "--organisation"): - clientdata["organisation"] = a - if o in ("-x", "--fax"): - clientdata["fax"] = a - if o in ("-p", "--phone"): - clientdata["phone"] = a - if verbose: - print "parsed client data is ", clientdata - try: - myclient = client.create(**clientdata) - except exceptions.CreationFailedError, cfe: - usage() - print cfe - sys.exit(2) - if verbose: - print myclient - -if __name__ == "__main__": - main() diff --git a/bin/gva b/bin/gva index b80a95f..542d727 100755 --- a/bin/gva +++ b/bin/gva @@ -24,14 +24,16 @@ import gnuviechadmin.cli.client import gnuviechadmin.cli.sysuser import sys +commands = [gnuviechadmin.cli.client.ClientCli, + gnuviechadmin.cli.sysuser.SysuserCli] + def usage(): print """%s <command> [commandargs] where command is one of - - client - for creating clients - sysuser - for creating system users """ % sys.argv[0] + for command in commands: + print "%10s - %s" % (command.name, command.description) def main(): if (sys.argv.__len__() < 2): @@ -39,12 +41,12 @@ def main(): sys.exit() command = sys.argv[1] commargs = sys.argv[2:] - if command == "client": - gnuviechadmin.cli.client.ClientCli(commargs) - elif command == "sysuser": - gnuviechadmin.cli.sysuser.SysuserCli(commargs) + if command in [cmd.name for cmd in commands]: + for cmd in commands: + if cmd.name == command: + cmd(commargs) else: - usage() + usage() if __name__ == '__main__': main() diff --git a/gnuviechadmin/backend/BackendEntity.py b/gnuviechadmin/backend/BackendEntity.py new file mode 100644 index 0000000..24fe6af --- /dev/null +++ b/gnuviechadmin/backend/BackendEntity.py @@ -0,0 +1,83 @@ +# -*- coding: UTF-8 -*- +# +# Copyright (C) 2007 by Jan Dittberner. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# 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. +# +# Version: $Id$ + +import ConfigParser, os +from subprocess import * +from sqlalchemy import * +from gnuviechadmin.exceptions import * + +class BackendEntity(object): + """This is the abstract base class for all backend entity classes.""" + + def __init__(self, verbose = False): + self.verbose = verbose + self.config = ConfigParser.ConfigParser() + self.config.readfp(open('gnuviechadmin/defaults.cfg')) + self.config.read(['gnuviechadmin/gva.cfg', + os.path.expanduser('~/.gva.cfg')]) + + def __repr__(self): + if self.verbose: + cols = [col for col in \ + object_mapper(self).local_table.columns.keys()] + format = "%(class)s:" + format = format + ", ".join([col + "=%(" + col + ")s" for col in \ + cols]) + data = {'class' : self.__class__.__name__} + else: + cols = self._shortkeys + format = ",".join("%(" + col + ")s" for col in cols) + data = {} + data.update(dict([(col, self.__getattribute__(col)) for col in cols])) + return format % data + + def sucommand(self, cmdline, pipedata = None): + """Executes a command as root using the configured suwrapper + command. If a pipe is specified it is used as stdin of the + subprocess.""" + suwrapper = self.config.get('common', 'suwrapper') + toexec = "%s %s" % (suwrapper, cmdline) + if pipedata: + p = Popen(toexec, shell = True, stdin=PIPE) + pipe = p.stdin + print >>pipe, pipedata + pipe.close() + sts = os.waitpid(p.pid, 0) + if self.verbose: + print "%s|%s: %d" % (pipedata, toexec, sts[1]) + else: + p = Popen(toexec, shell = True) + sts = os.waitpid(p.pid, 0) + if self.verbose: + print "%s: %s" % (toexec, sts[1]) + return sts[1] + + def validate(self): + """Validates whether all mandatory fields of the entity have + values.""" + missingfields = [] + for key in [col.name for col in \ + object_mapper(self).local_table.columns \ + if not col.primary_key and not col.nullable]: + if self.__getattribute__(key) is None: + missingfields.append(key) + if missingfields: + raise MissingFieldsError(missingfields) diff --git a/gnuviechadmin/backend/BackendEntityHandler.py b/gnuviechadmin/backend/BackendEntityHandler.py new file mode 100644 index 0000000..4238766 --- /dev/null +++ b/gnuviechadmin/backend/BackendEntityHandler.py @@ -0,0 +1,69 @@ +# -*- coding: UTF-8 -*- +# +# Copyright (C) 2007 by Jan Dittberner. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# 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. +# +# Version: $Id$ + +from sqlalchemy import * +from gnuviechadmin.exceptions import * +from BackendEntity import * + +class BackendEntityHandler(object): + def __init__(self, entityclass, verbose = False): + self.entityclass = entityclass + self.verbose = verbose + + def create(self, **kwargs): + try: + entity = self.entityclass(self.verbose, **kwargs) + sess = create_session() + sess.save(entity) + sess.flush() + try: + entity.create_hook() + except: + sess.delete(entity) + sess.flush() + raise + except Exception, e: + raise CreationFailedError(self.entityclass.__name__, e) + + def fetchall(self): + """Fetches all entities of the managed entity type.""" + session = create_session() + query = session.query(self.entityclass) + allentities = query.select() + for entity in allentities: + BackendEntity.__init__(entity, self.verbose) + return allentities + + def delete(self, pkvalue): + """Deletes the entity of the managed entity type that has the + specified primary key value.""" + try: + sess = create_session() + entity = sess.query(self.entityclass).get(pkvalue) + if entity: + BackendEntity.__init__(entity, self.verbose) + if self.verbose: + print "delete %s" % (str(entity)) + entity.delete_hook() + sess.delete(entity) + sess.flush() + except Exception, e: + raise DeleteFailedError(self.entityclass.__name__, e) diff --git a/bin/listclients b/gnuviechadmin/backend/__init__.py old mode 100755 new mode 100644 similarity index 83% rename from bin/listclients rename to gnuviechadmin/backend/__init__.py index c9c2998..9c370e3 --- a/bin/listclients +++ b/gnuviechadmin/backend/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/python # -*- coding: UTF-8 -*- # # Copyright (C) 2007 by Jan Dittberner. @@ -17,12 +16,10 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, # USA. +# +# Version: $Id$ -from gnuviechadmin import client +"""This is the gnuviechadmin.backend package. -def main(): - for row in client.fetchall(): - print row - -if __name__ == "__main__": - main() +The package provides the backend entities and supporting code for +gnuviechadmin.""" diff --git a/gnuviechadmin/backend/client.py b/gnuviechadmin/backend/client.py new file mode 100644 index 0000000..2aa07a0 --- /dev/null +++ b/gnuviechadmin/backend/client.py @@ -0,0 +1,59 @@ +# -*- coding: UTF-8 -*- +# +# Copyright (C) 2007 by Jan Dittberner. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# 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. +# +# Version: $Id$ + +from sqlalchemy import * +from tables import client_table +from gnuviechadmin.exceptions import * + +from BackendEntity import * +from BackendEntityHandler import * + +class Client(BackendEntity): + """Entity class for clients.""" + + _shortkeys = ('clientid', 'firstname', 'lastname', 'email') + + def __init__(self, verbose = False, **kwargs): + BackendEntity.__init__(self, verbose) + self.clientid = None + self.country = 'de' + self.title = None + self.address2 = None + self.mobile = None + self.fax = None + for item in kwargs.items(): + self.__setattr__(item) + self.validate() + + def create_hook(self): + pass + + def delete_hook(self): + pass + +client_mapper = mapper(Client, client_table) +client_mapper.add_property("sysusers", relation(sysuser.Sysuser)) + +class ClientHandler(BackendEntityHandler): + """BackendEntityHandler for Client entities.""" + + def __init__(self, verbose = False): + BackendEntityHandler.__init__(self, Client, verbose) diff --git a/gnuviechadmin/backend/domain.py b/gnuviechadmin/backend/domain.py new file mode 100644 index 0000000..7a13073 --- /dev/null +++ b/gnuviechadmin/backend/domain.py @@ -0,0 +1,50 @@ +# -*- coding: UTF-8 -*- +# +# Copyright (C) 2007 by Jan Dittberner. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# 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. +# +# Version: $Id: client.py 1101 2007-02-28 21:15:20Z jan $ + +from sqlalchemy import * +from tables import domain_table +from gnuviechadmin.exceptions import * + +import record +from BackendEntity import * +from BackendEntityHandler import * + +class Domain(BackendEntity): + """Entity class for DNS domains.""" + + _shortkeys = ("domainid", "sysuserid", "name", "type") + + def __init__(self, verbose = False, **kwargs): + BackendEntity.__init__(self, verbose) + self.domainid = None + self.sysuserid = None + self.name = None + self.type = None + self.validate() + +domain_mapper = mapper(Domain, domain_table) +domain_mapper.add_property("records", relation(record.Record)) + +class DomainHandler(BackendEntityHandler): + """BackendEntityHandler for Domain entities.""" + + def __init__(self, verbose = False): + BackendEntityHandler.__init__(self, Domain, verbose) diff --git a/gnuviechadmin/backend/record.py b/gnuviechadmin/backend/record.py new file mode 100644 index 0000000..d85f5e1 --- /dev/null +++ b/gnuviechadmin/backend/record.py @@ -0,0 +1,52 @@ +# -*- coding: UTF-8 -*- +# +# Copyright (C) 2007 by Jan Dittberner. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# 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. +# +# Version: $Id$ + +from sqlalchemy import * +from tables import record_table +from gnuviechadmin.exceptions import * + +from BackendEntity import * +from BackendEntityHandler import * + +class Record(BackendEntity): + """Entity class for DNS domain records.""" + + _shortkeys = ("recordid", "domainid", "name", "type", "content") + + def __init__(self, verbose = False, **kwargs): + BackendEntity.__init__(self, verbose) + self.recordid = None + self.domainid = None + self.name = None + self.type = None + self.content = None + self.ttl = None + self.prio = None + self.change_date = None + self.validate() + +record_mapper = mapper(Record, record_table) + +class RecordHandler(BackendEntityHandler): + """BackendEntityHandler for Record entities.""" + + def __init__(self, verbose = False): + BackendEntityHandler.__init__(self, Record, verbose) diff --git a/gnuviechadmin/backend/sysuser.py b/gnuviechadmin/backend/sysuser.py new file mode 100644 index 0000000..10bacf0 --- /dev/null +++ b/gnuviechadmin/backend/sysuser.py @@ -0,0 +1,153 @@ +# -*- coding: UTF-8 -*- +# +# Copyright (C) 2007 by Jan Dittberner. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# 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. +# +# Version: $Id$ + +from sqlalchemy import * +from tables import sysuser_table +from gnuviechadmin.exceptions import * +from gnuviechadmin.util import passwordutils, getenttools + +from BackendEntity import * +from BackendEntityHandler import * + +class Sysuser(BackendEntity): + """Entity class for system users.""" + + _shortkeys = ("sysuserid", "clientid", "username", "home", "shell") + + def __init__(self, verbose = False, **kwargs): + BackendEntity.__init__(self, verbose) + self.sysuserid = None + self.username = None + self.usertype = None + self.home = None + self.shell = None + self.clearpass = None + self.md5pass = None + self.clientid = None + self.sysuid = None + for key in kwargs.keys(): + self.__setattr__(key, kwargs[key]) + if not self.username: + self.username = self.getnextsysusername() + if not self.usertype: + self.usertype = self.getdefaultsysusertype() + if not self.home: + self.home = self.gethome(self.username) + if not self.shell: + self.shell = self.getdefaultshell() + (self.clearpass, self.md5pass) = \ + passwordutils.get_pw_tuple(self.clearpass) + if not self.sysuid: + self.sysuid = self.getnextsysuid() + self.validate() + + def getnextsysusername(self): + prefix = self.config.get('sysuser', 'nameprefix') + usernames = [user.username for user in \ + getenttools.finduserbyprefix(prefix)] + maxid = max([int(username[len(prefix):]) for username in usernames]) + for num in range(1, maxid + 1): + username = "%s%02d" % (prefix, num) + if not username in usernames: + return username + + def getdefaultsysusertype(self): + return 1 + + def gethome(self, sysusername): + """Gets a valid home directory for the given user name.""" + return os.path.join(self.config.get('sysuser', 'homedirbase'), + sysusername) + + def getdefaultshell(self): + return False + + def getshellbinary(self): + if self.shell: + return self.config.get('sysuser', 'shellyes') + return self.config.get('sysuser', 'shellno') + + def getnextsysuid(self): + uid = int(self.config.get('sysuser', 'minuid')) + muid = getenttools.getmaxuid(int(self.config.get('sysuser', + 'maxuid'))) + if muid >= uid: + uid = muid + 1 + return uid + + def populate_home(self): + templatedir = self.config.get('sysuser', 'hometemplate') + cmdline = 'install -d --owner="%(username)s" --group="%(group)s" "%(home)s"' % { + 'username' : self.username, + 'group' : self.config.get('sysuser', 'defaultgroup'), + 'home' : self.home } + self.sucommand(cmdline) + cmdline = 'cp -R "%(template)s" "%(home)s"' % { + 'template' : templatedir, + 'home' : self.home } + self.sucommand(cmdline) + cmdline = 'chown -R "%(username)s":"%(group)s" %(home)s' % { + 'username' : self.username, + 'group' : self.config.get('sysuser', 'defaultgroup'), + 'home' : self.home } + self.sucommand(cmdline) + + def create_hook(self): + gecos = self.config.get('sysuser', 'gecos') + gecos = gecos % (self.username) + cmdline = 'adduser --home "%(home)s" --shell "%(shell)s" --no-create-home --uid %(sysuid)d --ingroup "%(group)s" --disabled-password --gecos "%(gecos)s" %(username)s' % { + 'home' : self.home, + 'shell' : self.getshellbinary(), + 'sysuid' : self.sysuid, + 'group' : self.config.get('sysuser', 'defaultgroup'), + 'gecos' : gecos, + 'username' : self.username} + self.sucommand(cmdline) + cmdline = 'chpasswd --encrypted' + inline = '%(username)s:%(md5pass)s' % { + 'username' : self.username, + 'md5pass' : self.md5pass} + self.sucommand(cmdline, inline) + self.populate_home() + + def delete_hook(self): + backupdir = os.path.join(self.config.get('common', + 'backupdir'), + self.config.get('sysuser', + 'homebackupdir')) + if not os.path.isdir(backupdir): + cmdline = 'mkdir -p "%(backupdir)s"' % { + 'backupdir' : backupdir} + status = self.sucommand(cmdline) + if status != 0: + raise Exception("could not create backup directory") + cmdline = 'deluser --remove-home --backup --backup-to "%(backupdir)s" %(username)s' % { + 'backupdir' : backupdir, + 'username' : self.username} + self.sucommand(cmdline) + +sysusermapper = mapper(Sysuser, sysuser_table) + +class SysuserHandler(BackendEntityHandler): + """BackendEntityHandler for Sysuser entities.""" + + def __init__(self, verbose = False): + BackendEntityHandler.__init__(self, Sysuser, verbose) diff --git a/gnuviechadmin/tables.py b/gnuviechadmin/backend/tables.py similarity index 66% rename from gnuviechadmin/tables.py rename to gnuviechadmin/backend/tables.py index f254a7d..c02f3d0 100644 --- a/gnuviechadmin/tables.py +++ b/gnuviechadmin/backend/tables.py @@ -52,9 +52,42 @@ sysuser_table = Table( Column('home', String(128)), Column('shell', Boolean, nullable=False, default=False), Column('clearpass', String(64)), - Column('md5pass', String(32)), + Column('md5pass', String(34)), Column('clientid', Integer, ForeignKey("client.clientid"), nullable=False), Column('sysuid', Integer, nullable=False, unique=True), Column('lastchange', DateTime, default=func.now()) ) sysuser_table.create(checkfirst=True) + +domain_table = Table( + 'domain', meta, + Column('domainid', Integer, primary_key=True), + Column('name', String(255), nullable=False, unique=True), + Column('master', String(20)), + Column('last_check', Integer), + Column('type', String(6), nullable=False), + Column('notified_serial', Integer), + Column('sysuserid', Integer, ForeignKey("sysuser.sysuserid"), + nullable=False)) +domain_table.create(checkfirst=True) + +record_table = Table( + 'record', meta, + Column('recordid', Integer, primary_key=True), + Column('domainid', Integer, ForeignKey("domain.domainid"), + nullable=False), + Column('name', String(255)), + Column('type', String(6)), + Column('content', String(255)), + Column('ttl', Integer), + Column('prio', Integer), + Column('change_date', Integer)) +record_table.create(checkfirst=True) + +supermaster_table = Table( + 'supermaster', meta, + Column('ip', String(25), nullable=False), + Column('nameserver', String(255), nullable=False), + Column('account', Integer, ForeignKey("sysuser.sysuserid"), + nullable=False)) +supermaster_table.create(checkfirst=True) diff --git a/gnuviechadmin/cli/CliCommand.py b/gnuviechadmin/cli/CliCommand.py index c2f994e..fcc3646 100644 --- a/gnuviechadmin/cli/CliCommand.py +++ b/gnuviechadmin/cli/CliCommand.py @@ -20,62 +20,202 @@ # Version: $Id$ import getopt, sys +from gnuviechadmin.exceptions import GnuviechadminError class CliCommand: - """Base class for command line interface.""" - def usage(self): - """This method should print usage information for the command.""" + """Base class for command line interface. A specific + implementation class must define the fields name, description and + _optionmap. + + + The field name is the name of the subcommand. + + The field description is a short, one line description of the + command. + + The field _optionmap is a map which maps the subcommand names to + lists of tuples. Each tuple consists of four elements. The first + element is a list of command line arguments, short arguments start + with dash, long arguments with a double dash. The second element + is the name of the field in the data map of the command, it will + directly be sent to the underlying entity. The third field is a + description for the group of command line options in field + one. The fourth field is True for mandatory fields and False + otherwise. + """ + + def _usage(self): + """This method shows usage information. The implementation + relies on the information in the fields name, description and + _optionmap in the implementation classes.""" + print """GNUViechAdmin command line interface + +Subcommand: %(command)s + + %(description)s + +Usage: + +%(called)s %(command)s -h|--help + + gives this usage information. + +Common options: + + %(option)s + %(mandatory)s %(optiondesc)s +""" % { 'called' : sys.argv[0], + 'command' : self.name, + 'description' : self.description, + 'option' : '-v, --verbose', + 'optiondesc' : 'verbose operation', + 'mandatory' : " "} + for commandname in self._optionmap.keys(): + cmdl = "%(called)s %(command)s %(subcommand)s [-v|--verbose]" % { + 'called' : sys.argv[0], + 'command' : self.name, + 'subcommand' : commandname} + desc = """ + %s +""" % (self._optionmap[commandname][0]) + for (options, field, optiondesc, mandatory) in \ + self._optionmap[commandname][1]: + cmd = " " + pairs = [] + for option in options: + if field: + if option.startswith("--"): + pairs.append("%s=<%s>" % (option, field)) + else: + pairs.append("%s <%s>" % (option, field)) + else: + pairs.append(option) + if not mandatory: + cmd = cmd + "[" + cmd = cmd + "|".join(pairs) + if not mandatory: + cmd = cmd + "]" + descmap = { + 'option' : ", ".join(pairs), + 'optiondesc' : optiondesc, + 'mandatory' : ' '} + if mandatory: + descmap['mandatory'] = '*' + desc = desc + """ %(option)s + %(mandatory)s %(optiondesc)s +""" % descmap + if (len(cmdl) + len(cmd)) > 79: + print cmdl + cmdl = cmd + else: + cmdl = cmdl + cmd + print cmdl + print desc + print "Mandatory options are marked with *" + + def _subcommands(self): + """Convenience method for retrieving the subcommand names from + the _optionmap field.""" + return self._optionmap.keys() + + def _longopts(self, subcommand): + """This method retrieves available long options in a format + valid for getopt.gnu_getopt(...) from the _optionmap field.""" + longopts = [] + for cur in [(option[0], option[1]) for option in \ + self._optionmap[subcommand][1]]: + for command in cur[0]: + if command.startswith("--"): + if cur[1]: + longopts.append(command[2:] + "=") + else: + longopts.append(command[2:]) + return longopts + + def _shortopts(self, subcommand): + """This method retrieves available short options in a format + valid for getopt.gnu_getopt(...) from the _optionmap field.""" + shortopts = "" + for cur in [(option[0], option[1]) for option in \ + self._optionmap[subcommand][1]]: + for command in cur[0]: + if not command.startswith("--"): + if cur[1]: + shortopts = shortopts + command[1:] + ":" + else: + shortopts = shortopts + command[1:] + return shortopts + + def _checkrequired(self, subcommand): + """Checks whether the required fields of the given subcommand + are set.""" + reqcheck = [True, []] + for req in [option for option in \ + self._optionmap[subcommand][1] if option[3]]: + if not req[1] in self._data: + reqcheck[0] = False + reqcheck[1].append(""" %s + * %s""" % (", ".join(req[0]), req[2])) + return reqcheck + + def _handleoption(self, subcommand, o, a): + """Handles a command line option by assigning it to the + matching key as defined in the _optionmap property of the + implementation class.""" + optionmap = [(option[0], option[1]) for option in \ + self._optionmap[subcommand][1]] + if optionmap: + for (options, datakey) in optionmap: + if o in options: + self._data[datakey] = a + + def _execute(self, subcommand): + """This method is called when the subcommand of the command is + executed.""" raise NotImplementedError - def shortopts(self): - """This method should return an option string for the short - options for getopt.gnu_getopt(...).""" - raise NotImplementedError - - def longopts(self): - """This method should return a list of long options for - getopt.gnu_getopt(...).""" - raise NotImplementedError - - def handleoption(self, option, argument): - """This method should handle each option known to the command.""" - raise NotImplementedError - - def execute(self): - """This method is called when the command is executed.""" - raise NotImplementedError - - def checkrequired(self): - """This methode is called after handling command line options - and should check whether all required values were set.""" - raise NotImplementedError - - def __parseopts(self, args): + def _parseopts(self, subcommand, args): """This method parses the options given on the command line.""" longopts = ["help", "verbose"] - longopts.extend(self.longopts()) + longopts.extend(self._longopts(subcommand)) try: opts, args = getopt.gnu_getopt( args, - "hv" + self.shortopts(), + "hv" + self._shortopts(subcommand), longopts) except getopt.GetoptError: - self.usage() + self._usage() sys.exit(2) - self.verbose = False + self._verbose = False for o, a in opts: if o in ("-v", "--verbose"): - self.verbose = True + self._verbose = True if o in ("-h", "--help"): - self.usage() + self._usage() sys.exit() - self.handleoption(o, a) + self._handleoption(subcommand, o, a) def __init__(self, args): """This initializes the command with the given command line arguments and executes it.""" - self.__parseopts(args) - if (self.checkrequired()): - self.execute() + self._data = {} + if len(args) > 0: + if args[0] in self._subcommands(): + self._parseopts(args[0], args[1:]) + reqcheck = self._checkrequired(args[0]) + if reqcheck[0]: + try: + self._execute(args[0]) + except GnuviechadminError, e: + print e + else: + self._usage() + print """ +the following required arguments are missing: +""" + print "\n".join(reqcheck[1]) + else: + self._usage() + print "invalid sub command" else: - self.usage() + self._usage() diff --git a/gnuviechadmin/cli/client.py b/gnuviechadmin/cli/client.py index 646bd68..536e37c 100644 --- a/gnuviechadmin/cli/client.py +++ b/gnuviechadmin/cli/client.py @@ -20,91 +20,63 @@ # Version: $Id$ import CliCommand, sys -from gnuviechadmin import client class ClientCli(CliCommand.CliCommand): - """Command line interface command for client creation.""" - def shortopts(self): - return "f:l:a:z:c:e:p:o:t:m:x:" + """Command line interface command for client managament.""" - def longopts(self): - return ["firstname=", "lastname=", "address=", "zip=", - "city=", "email=", "phone=", "address2=", "organisation=", - "country=", "title=", "mobile=", "fax="] - - def usage(self): - print """Usage: %s client [-h|--help] [-v|--verbose] - [-t <title>|--title=<title>] - -f <firstname>|--firstname=<firstname> -l <lastname>|--lastname=<lastname> - -a <address1>|--address=<address1> [--address2=<address2>] - -z <zip>|--zip=<zip> -c <city>|--city=<city> [--country=<isocode>] - [-o <organisation>|--organisation=<organisation>] - -e <email>|--email=<email> -p <phone>|--phone=<phone> - [-m <mobile>|--mobile=<mobile>] [-x <fax>|--fax=<fax>] - -General options: - -h, --help show this usage message and exit - -v, --verbose verbose operation - -Mandatory client data options: - -f, --firstname firstname - -l, --lastname lastname - -a, --address street address - -z, --zip zip or postal code - -c, --city city or location - -e, --email contact email address - -p, --phone telephone number - -Optional client data options: - --address2 optional second line of the street address - -o, --organisation option organisation - --country country (defaults to de) - -t, --title optional title - -m, --mobile optional mobile number - -x, --fax optional fax number -""" % sys.argv[0] - - def handleoption(self, o, a): - if o in ("-f", "--firstname"): - self.data["firstname"] = a - elif o in ("-l", "--lastname"): - self.data["lastname"] = a - elif o in ("-a", "--address"): - self.data["address1"] = a - elif o in ("-z", "--zip"): - self.data["zip"] = a - elif o in ("-c", "--city"): - self.data["city"] = a - elif o == "--country": - self.data["country"] = a - elif o in ("-t", "--title"): - self.data["title"] = a - elif o in ("-m", "--mobile"): - self.data["mobile"] = a - elif o in ("-e", "--email"): - self.data["email"] = a - elif o in ("-o", "--organisation"): - self.data["organisation"] = a - elif o in ("-x", "--fax"): - self.data["fax"] = a - elif o in ("-p", "--phone"): - self.data["phone"] = a - - def checkrequired(self): - required = ['firstname', 'lastname', 'address1', 'zip', 'city', - 'phone', 'email'] - if self.verbose: - print self.data - for req in required: - if not req in self.data: - return False - return True - - def execute(self): - myclient = client.create(**self.data) - if self.verbose: - print myclient + name = "client" + description = "manage clients" + _optionmap = { + 'create' : ("creates a new client", + [(["-f", "--firstname"], "firstname", + "the client's first name", True), + (["-l", "--lastname"], "lastname", + "the client's last name", True), + (["-t", "--title"], "title", + "the client's title", False), + (["-a", "--address"], "address1", + "the address of the client", True), + (["--address2"], "address2", + "second line of the client's address", False), + (["-z", "--zip"], "zip", + "the zipcode of the client's address", True), + (["-c", "--city"], "city", + "the city of the client's address", True), + (["--country"], "country", + "the client's country", False), + (["-e", "--email"], "email", + "the client's email address", True), + (["-p", "--phone"], "phone", + "the client's phone number", True), + (["-m", "--mobile"], "mobile", + "the client's mobile phone number", False), + (["-x", "--fax"], "fax", + "the client's fax number", False)]), + 'list' : ("lists existing clients", + []), + 'delete' : ("deletes the specified client if it has no dependent data", + [(["-c", "--clientid"], "clientid", + "the client id", True)])} + + def _execute(self, subcommand): + from gnuviechadmin.backend import client + from gnuviechadmin import exceptions + if subcommand == "create": + try: + myclient = client.ClientHandler(self._verbose).create( + **self._data) + if self._verbose: + print myclient + except exceptions.CreationFailedError, cfe: + self._usage() + print cfe + sys.exit(2) + elif subcommand == "list": + clients = client.ClientHandler(self._verbose).fetchall() + for client in clients: + print client + elif subcommand == "delete": + client.ClientHandler(self._verbose).delete(self._data["clientid"]) def __init__(self, argv): - self.data = {} CliCommand.CliCommand.__init__(self, argv) diff --git a/gnuviechadmin/cli/sysuser.py b/gnuviechadmin/cli/sysuser.py index 2ae2839..27ac177 100644 --- a/gnuviechadmin/cli/sysuser.py +++ b/gnuviechadmin/cli/sysuser.py @@ -1 +1,71 @@ -pass +# -*- coding: UTF-8 -*- +# +# Copyright (C) 2007 by Jan Dittberner. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# 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. +# +# Version: $Id$ + +import CliCommand, sys + +class SysuserCli(CliCommand.CliCommand): + """Command line interface command for system user managament.""" + + name = "sysuser" + description = "manage system users" + _optionmap = { + "create" : ("create a new system user with the given options.", + [(["-n", "--username"], "username", + "the system user name", False), + (["-t", "--usertype"], "usertype", + "the numeric user type", False), + (["-h", "--home"], "home", + "the home directory", False), + (["-s", "--shell"], "shell", + "true if the user should get shell access", False), + (["-p", "--password"], "clearpass", + "the password for the user", False), + (["-c", "--clientid"], "clientid", + "the client id", True)]), + "list" : ("list existing system users.", + []), + "delete" : ("delete a system user.", + [(["-s", "--sysuserid"], "sysuserid", + "the system user id", True)])} + + def _execute(self, subcommand): + from gnuviechadmin.backend import sysuser + from gnuviechadmin import exceptions + if subcommand == "create": + try: + mysysuser = sysuser.SysuserHandler(self._verbose).create( + **self._data) + if self._verbose: + print mysysuser + except exceptions.CreationFailedError, cfe: + self._usage() + print cfe + sys.exit(2) + elif subcommand == "list": + sysusers = sysuser.SysuserHandler(self._verbose).fetchall() + for su in sysusers: + print su + elif subcommand == "delete": + sysuser.SysuserHandler(self._verbose).delete( + self._data["sysuserid"]) + + def __init__(self, argv): + CliCommand.CliCommand.__init__(self, argv) diff --git a/gnuviechadmin/client.py b/gnuviechadmin/client.py deleted file mode 100644 index 3cf1852..0000000 --- a/gnuviechadmin/client.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: UTF-8 -*- -# -# Copyright (C) 2007 by Jan Dittberner. -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# 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. -# -# Version: $Id$ - -from sqlalchemy import * -from gnuviechadmin.tables import * -from gnuviechadmin import sysuser -from gnuviechadmin.exceptions import * - -class Client(object): - mandatory = ('firstname', 'lastname', 'address1', 'zip', 'city', - 'country', 'phone', 'email') - - """This class provides a client representation""" - def __init__(self, **kwargs): - self.clientid = None - self.country = 'de' - self.title = None - self.address2 = None - self.mobile = None - self.fax = None - self.organisation = None - for key in kwargs.keys(): - self.__setattr__(key, kwargs[key]) - self.validate() - - def validate(self): - missingfields = [] - for key in self.mandatory: - if self.__getattribute__(key) is None: - missingfields.append(key) - if missingfields: - raise MissingFieldsError(missingfields) - - def __repr__(self): - return "%(clientid)d,%(firstname)s,%(lastname)s,%(email)s" % ( - {'clientid' : self.clientid, 'firstname' : self.firstname, - 'lastname' : self.lastname, 'email' : self.email}) - -clientmapper = mapper( - Client, client_table, - properties = {'sysusers': relation(sysuser.Sysuser)}) - -def create(**kwargs): - try: - myclient = Client(**kwargs) - sess = create_session() - sess.save(myclient) - sess.flush() - except MissingFieldsError, mfe: - raise CreationFailedError(Client.__name__, mfe) - except exceptions.SQLError, sqle: - raise CreationFailedError(Client.__name__, sqle) - return myclient - -def fetchall(): - session = create_session() - query = session.query(Client) - return query.select() diff --git a/gnuviechadmin/defaults.cfg b/gnuviechadmin/defaults.cfg index fe1f85f..b9f487e 100644 --- a/gnuviechadmin/defaults.cfg +++ b/gnuviechadmin/defaults.cfg @@ -32,3 +32,19 @@ # very usable for a real installation. # uri = sqlite:///:memory: + +[common] +suwrapper = sudo +backupdir = /var/backups/gnuviechadmin + +[sysuser] +nameprefix = usr +homedirbase = /home/www +minuid = 10000 +maxuid = 39999 +shellyes = /bin/bash +shellno = /usr/bin/scponly +defaultgroup = wwwusers +gecos = Webuser %s +homebackupdir = homes +hometemplate = /etc/gnuviechadmin/templates/home diff --git a/gnuviechadmin/exceptions.py b/gnuviechadmin/exceptions.py index 34e2002..a8dc0b4 100644 --- a/gnuviechadmin/exceptions.py +++ b/gnuviechadmin/exceptions.py @@ -20,18 +20,24 @@ # Version: $Id$ """This file defines the gnuviechadmin specific exception types.""" -class MissingFieldsError(Exception): + +class GnuviechadminError(Exception): + """This is the base class for domain specific exceptions of + Gnuviechadmin.""" + pass + +class MissingFieldsError(GnuviechadminError): """This exception should be raised when a required field of a data -class is missing.""" + class is missing.""" def __init__(self, missingfields): self.missing = missingfields def __str__(self): return "the fields %s are missing." % (repr(self.missing)) -class CreationFailedError(Exception): +class CreationFailedError(GnuviechadminError): """This exception should be raised if a business object could not -be created.""" + be created.""" def __init__(self, classname, cause = None): self.classname = classname self.cause = cause @@ -41,3 +47,16 @@ be created.""" if self.cause: msg += " The reason is %s." % (str(self.cause)) return msg + +class DeleteFailedError(GnuviechadminError): + """This exception should be raise if a business object coild not + be deleted.""" + def __init__(self, classname, cause = None): + self.classname = classname + self.cause = cause + + def __str__(self): + msg = "Deleting an instance of class %s failed." % (self.classname) + if self.cause: + msg += " The reason is %s." % (str(self.cause)) + return msg diff --git a/gnuviechadmin/sysuser.py b/gnuviechadmin/util/__init__.py similarity index 65% rename from gnuviechadmin/sysuser.py rename to gnuviechadmin/util/__init__.py index 4714217..3fcfaa4 100644 --- a/gnuviechadmin/sysuser.py +++ b/gnuviechadmin/util/__init__.py @@ -19,16 +19,6 @@ # # Version: $Id$ -from sqlalchemy import * -from gnuviechadmin.tables import * -from gnuviechadmin.exceptions import * +"""This is the gnuviechadmin.util package. -class Sysuser(object): - def __repr__(self): - return "%(sysuserid)d,%(username)s,%(clientid)d,%(sysuid)d" % ({ - 'sysuserid': self.sysuserid, - 'username': self.username, - 'clientid': self.clientid, - 'sysuid': self.sysuid}) - -sysusermapper = mapper(Sysuser, sysuser_table) +The package provides utility modules for various functions.""" diff --git a/gnuviechadmin/util/getenttools.py b/gnuviechadmin/util/getenttools.py new file mode 100644 index 0000000..be3ed1f --- /dev/null +++ b/gnuviechadmin/util/getenttools.py @@ -0,0 +1,94 @@ +# -*- coding: UTF-8 -*- +# +# Copyright (C) 2007 by Jan Dittberner. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# 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. +# +# Version: $Id$ + +import os, popen2 + +class PasswdUser(object): + """This class represents users in the user database.""" + def __init__(self, username, pw, uid, gid, gecos, home, shell): + self.username = username + self.uid = int(uid) + self.gid = int(gid) + self.gecos = gecos + self.home = home + self.shell = shell + + def __repr__(self): + return "%s(%s:%d:%d:%s:%s:%s)" % (self.__class__.__name__, + self.username, + self.uid, + self.gid, + self.gecos, + self.home, + self.shell) + +class PasswdGroup(object): + """This class represents lines in the groups database.""" + def __init__(self, groupname, pw, gid, members): + self.groupname = groupname + self.gid = int(gid) + self.members = members.split(",") + + def __repr__(self): + return "%s(%s:%d:%s)" % (self.__class__.__name__, + self.groupname, + self.gid, + ",".join(self.members)) + +def parsegroups(): + (stdout, stdin) = popen2.popen2("getent group") + return [PasswdGroup(*arr) for arr in [line.strip().split(":") for line in stdout]] + +def parseusers(): + (stdout, stdin) = popen2.popen2("getent passwd") + return [PasswdUser(*arr) for arr in [line.strip().split(":") for line in stdout]] + +def finduserbyprefix(prefix): + """Finds all user entries with the given prefix.""" + return [user for user in parseusers() if user.username.startswith(prefix)] + +def getuserbyid(uid): + """Gets the user with the given user id.""" + users = [user for user in parseusers() if user.uid == uid] + if users: + return users[0] + return None + +def getgroupbyid(gid): + """Gets the group with the given group id.""" + groups = [group for group in parsegroups() if group.gid == gid] + if groups: + return groups[0] + return None + +def getmaxuid(boundary = 65536): + """Gets the highest uid value.""" + return max([user.uid for user in parseusers() if user.uid <= boundary]) + +def getmaxgid(boundary = 65536): + """Gets the highest gid value.""" + return max([group.gid for group in parsegroups() if group.gid <= boundary]) + +if __name__ == "__main__": + print "Max UID is %d" % (getmaxuid(40000)) + print "Max GID is %d" % (getmaxgid(40000)) + print "User with max UID is %s" % (getuserbyid(getmaxuid(40000))) + print "Group with max GID is %s" % (getgroupbyid(getmaxgid(40000))) diff --git a/gnuviechadmin/util/passwordutils.py b/gnuviechadmin/util/passwordutils.py new file mode 100644 index 0000000..111e2c6 --- /dev/null +++ b/gnuviechadmin/util/passwordutils.py @@ -0,0 +1,57 @@ +# -*- coding: UTF-8 -*- +# +# Copyright (C) 2007 by Jan Dittberner. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# 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. +# +# Version: $Id$ + +import crypt, crack, random + +def generatepassword(minlength = 8, maxlength = 12): + """Generates a random password with a length between the given + minlength and maxlength values.""" + pwchars = [] + for pair in (('0', '9'), ('A', 'Z'), ('a', 'z')): + pwchars.extend(range(ord(pair[0]), ord(pair[1]))) + for char in "-+/*_@": + pwchars.append(ord(char)) + return "".join([chr(letter) for letter in \ + random.sample(pwchars, + random.randint(minlength, maxlength))]) + +def checkpassword(password): + """Checks the password with cracklib. The password is returned if + it is good enough. Otherwise None is returned.""" + try: + return crack.VeryFascistCheck(password) + except ValueError, ve: + print "Weak password:", ve + return None + +def md5_crypt_password(password): + """Hashes the given password with MD5 and a random salt value.""" + salt = "".join([chr(letter) for letter in \ + random.sample(range(ord('a'), ord('z')), 8)]) + return crypt.crypt(password, '$1$' + salt) + +def get_pw_tuple(password = None): + """Gets a valid tuple consisting of a password and a md5 hash of the + password. If a password is given it is checked and if it is too weak + replaced by a generated one.""" + while password == None or checkpassword(password) == None: + password = generatepassword() + return (password, md5_crypt_password(password))