From fdea3217c83136045805e7549766eabf9d17147d Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Mon, 9 Jul 2007 06:46:36 +0000 Subject: [PATCH] - mail related tables in database schema - gpg encryption for mails - domain creation and deletion completed - logging - use pwd and grp git-svn-id: file:///home/www/usr01/svn/gnuviechadmin/gnuviech.info/gnuviechadmin/trunk@230 a67ec6bc-e5d5-0310-a910-815c51eb3124 --- bin/gva | 7 +- data/dbrepo/versions/3/3.py | 49 +++ data/templates/domain/index.html | 6 + data/templates/domainconf/htaccess-stats | 4 + data/templates/domainconf/modlogan.conf | 42 +++ data/templates/domainconf/vhost.conf | 10 + data/templates/mails/create_client.txt | 8 + data/templates/mails/create_domain.txt | 14 + data/templates/mails/create_sysuser.txt | 8 + gnuviechadmin/backend/BackendEntity.py | 101 +++++-- gnuviechadmin/backend/BackendEntityHandler.py | 75 +++-- gnuviechadmin/backend/BackendTo.py | 73 +++++ gnuviechadmin/backend/client.py | 60 ++-- gnuviechadmin/backend/domain.py | 281 +++++++++++++++--- gnuviechadmin/backend/record.py | 35 +-- gnuviechadmin/backend/settings.py | 19 +- gnuviechadmin/backend/sysuser.py | 151 +++++----- gnuviechadmin/backend/tables.py | 99 ++---- gnuviechadmin/cli/CliCommand.py | 4 +- gnuviechadmin/cli/client.py | 2 + gnuviechadmin/cli/domain.py | 2 + gnuviechadmin/cli/record.py | 11 +- gnuviechadmin/cli/sysuser.py | 2 + gnuviechadmin/defaults.cfg | 32 +- gnuviechadmin/exceptions.py | 13 + gnuviechadmin/logging.cfg | 34 +++ gnuviechadmin/util/getenttools.py | 43 ++- gnuviechadmin/util/passwordutils.py | 15 +- 28 files changed, 877 insertions(+), 323 deletions(-) create mode 100644 data/dbrepo/versions/3/3.py create mode 100644 data/templates/domain/index.html create mode 100644 data/templates/domainconf/htaccess-stats create mode 100644 data/templates/domainconf/modlogan.conf create mode 100644 data/templates/domainconf/vhost.conf create mode 100644 data/templates/mails/create_client.txt create mode 100644 data/templates/mails/create_domain.txt create mode 100644 data/templates/mails/create_sysuser.txt create mode 100644 gnuviechadmin/backend/BackendTo.py create mode 100644 gnuviechadmin/logging.cfg diff --git a/bin/gva b/bin/gva index f4aebcd..927bfcb 100755 --- a/bin/gva +++ b/bin/gva @@ -24,7 +24,12 @@ import gnuviechadmin.cli.client import gnuviechadmin.cli.sysuser import gnuviechadmin.cli.domain import gnuviechadmin.cli.record -import sys +import sys, os, logging.config + +logcfgs = ('gnuviechadmin/logging.cfg', '/etc/gnuviechadmin/logging.cfg', + os.path.expanduser('~/.gva-logging.cfg')) +for cfg in [x for x in logcfgs if os.path.exists(x)]: + logging.config.fileConfig(cfg) commands = [gnuviechadmin.cli.client.ClientCli, gnuviechadmin.cli.sysuser.SysuserCli, diff --git a/data/dbrepo/versions/3/3.py b/data/dbrepo/versions/3/3.py new file mode 100644 index 0000000..c634354 --- /dev/null +++ b/data/dbrepo/versions/3/3.py @@ -0,0 +1,49 @@ +from sqlalchemy import * +from migrate import * +from gnuviechadmin.backend.settings import dbschema + +meta = BoundMetaData(migrate_engine) +domain = Table('domain', meta, schema = dbschema, autoload = True) +mailaccount = Table( + 'mailaccount', meta, + Column('mailaccountid', Integer, primary_key = True), + Column('domainid', Integer, ForeignKey('domain.domainid'), + nullable = False), + Column('mailaccount', String(12), nullable = False, unique = True), + Column('clearpass', String(64)), + Column('cryptpass', String(34)), + Column('uid', Integer, nullable = False), + Column('gid', Integer, nullable = False), + Column('home', String(128), nullable = False), + Column('spamcheck', Boolean, nullable = False, default = False), + Column('sajunkscore', Integer), + schema = dbschema + ) +mailaddress = Table( + 'mailaddress', meta, + Column('mailaddressid', Integer, primary_key = True), + Column('domainid', Integer, ForeignKey('domain.domainid'), + nullable = False), + Column('email', String(255), nullable = False), + UniqueConstraint('email', 'domainid'), + schema = dbschema + ) +mailtarget = Table( + 'mailtarget', meta, + Column('mailtargetid', Integer, primary_key = True), + Column('mailaddressid', Integer, ForeignKey('mailaddress.mailaddressid'), + nullable = False), + Column('target', String(128), nullable = False), + UniqueConstraint('target', 'mailaddressid'), + schema = dbschema + ) + +def upgrade(): + mailaccount.create() + mailaddress.create() + mailtarget.create() + +def downgrade(): + mailtarget.drop() + mailaddress.drop() + mailaccount.drop() diff --git a/data/templates/domain/index.html b/data/templates/domain/index.html new file mode 100644 index 0000000..76f97c4 --- /dev/null +++ b/data/templates/domain/index.html @@ -0,0 +1,6 @@ + +Hier entsteht ein neuer Internetauftritt + +

Hier entsteht der Internetauftritt für www.${domain}.

+ + diff --git a/data/templates/domainconf/htaccess-stats b/data/templates/domainconf/htaccess-stats new file mode 100644 index 0000000..0fea634 --- /dev/null +++ b/data/templates/domainconf/htaccess-stats @@ -0,0 +1,4 @@ +AuthType Basic +AuthName "Statistics for ${domain}" +AuthUserFile "${userfile}" +require valid-user diff --git a/data/templates/domainconf/modlogan.conf b/data/templates/domainconf/modlogan.conf new file mode 100644 index 0000000..782be47 --- /dev/null +++ b/data/templates/domainconf/modlogan.conf @@ -0,0 +1,42 @@ +[global] +includepath = /etc/modlogan +include = modlogan.def.conf,global + +loadplugin = input_clf +loadplugin = processor_web +loadplugin = output_modlogan + +statedir = ${statsdir} + +incremental = 1 +enable_resolver = 1 + +read_ahead_limit = 100 + +[processor_web] +# to group known bot attacks like Nimda and CodeRed +include = group.url.conf,group_exploits +# to use the german file extension descriptions uncomment the following +#include = group.extension.conf,groupext_german +# include the default config +include = modlogan.def.conf,processor_web + +# to only have pages listed, not files +#hideurl="\.(?i:gif|png|jpe?g|css|js|class|mid|swf|mp3|mpg)$$" +# to not show people they're reading the stats more often than people their page... +hideurl="^/stats" + +debug_searchengines = 1 +hidereferrer = "^http://([^.]+\.)*${domainesc}/" + +## configure the output generator +[output_modlogan] +include = modlogan.def.conf,output_modlogan +hostname = ${domain} + +outputdir = ${statsdir} + +## configure the parser +[input_clf] +include = modlogan.def.conf,input_clf +inputfile = - diff --git a/data/templates/domainconf/vhost.conf b/data/templates/domainconf/vhost.conf new file mode 100644 index 0000000..9f15ffa --- /dev/null +++ b/data/templates/domainconf/vhost.conf @@ -0,0 +1,10 @@ + + ServerName www.${domain} + ServerAlias ${domain} + + Alias /stats ${statsdir} + DocumentRoot ${docroot} + + ErrorLog ${logdir}/error.log + CustomLog ${logdir}/access.log combined + \ No newline at end of file diff --git a/data/templates/mails/create_client.txt b/data/templates/mails/create_client.txt new file mode 100644 index 0000000..0988a6f --- /dev/null +++ b/data/templates/mails/create_client.txt @@ -0,0 +1,8 @@ +A new client with the following data has been created: + + Firstname : ${firstname} + Lastname : ${lastname} + Email : ${email} + Address : ${address1} + : ${zipcode} ${city} + Phone : ${phone} diff --git a/data/templates/mails/create_domain.txt b/data/templates/mails/create_domain.txt new file mode 100644 index 0000000..d876205 --- /dev/null +++ b/data/templates/mails/create_domain.txt @@ -0,0 +1,14 @@ +A new domain with the following data has been created: + + System user : ${sysuser} + Domain name : ${domain} + Document root : ${docroot} + Statistics password : ${statspass} + +To enable the domain in apache use + + sudo a2ensite ${domain} + +You can access statistics for the domain at + + http://www.${domain}/stats/ diff --git a/data/templates/mails/create_sysuser.txt b/data/templates/mails/create_sysuser.txt new file mode 100644 index 0000000..eb08125 --- /dev/null +++ b/data/templates/mails/create_sysuser.txt @@ -0,0 +1,8 @@ +A new system user has been created + +UID : ${uid} +Client : ${firstname} ${lastname} <${email}> +Username : ${username} +Password : ${password} +Home : ${home} +Shell : ${shell} diff --git a/gnuviechadmin/backend/BackendEntity.py b/gnuviechadmin/backend/BackendEntity.py index 24fe6af..06a02e4 100644 --- a/gnuviechadmin/backend/BackendEntity.py +++ b/gnuviechadmin/backend/BackendEntity.py @@ -19,41 +19,35 @@ # # Version: $Id$ -import ConfigParser, os -from subprocess import * -from sqlalchemy import * +import smtplib, os, logging, tempfile +from email.MIMEText import MIMEText +from pyme import core +from pyme.constants.sig import mode + +from settings import config from gnuviechadmin.exceptions import * +from subprocess import * +import sqlalchemy class BackendEntity(object): """This is the abstract base class for all backend entity classes.""" - def __init__(self, verbose = False): + def __init__(self, delegateto, verbose = False): + self.logger = logging.getLogger("%s.%s" % ( + self.__class__.__module__, self.__class__.__name__)) + self.delegateto = delegateto 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 + return self.delegateto.__repr__(verbose = self.verbose) 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') + self.logger.debug("sucommand called: %s (pipedata=%s)", cmdline, + str(pipedata)) + suwrapper = config.get('common', 'suwrapper') toexec = "%s %s" % (suwrapper, cmdline) if pipedata: p = Popen(toexec, shell = True, stdin=PIPE) @@ -63,21 +57,80 @@ class BackendEntity(object): sts = os.waitpid(p.pid, 0) if self.verbose: print "%s|%s: %d" % (pipedata, toexec, sts[1]) + self.logger.info("%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]) + self.logger.info("%s: %s", toexec, sts[1]) return sts[1] + def send_mail(self, subject, text): + """This method sends a mail with the given text and subject + and signs it usign GnuPG. If a public key of the recipient is + available the mail is encrypted.""" + plain = core.Data(text) + sig = core.Data() + c = core.Context() + signer = config.get('common', 'mailfrom') + rcpt = config.get('common', 'mailto') + c.signers_clear() + for sigkey in [x for x in c.op_keylist_all(signer, 1)]: + if sigkey.can_sign: + c.signers_add(sigkey) + if not c.signers_enum(0): + raise Exception("No secret keys for signing available for %s." % ( + signer)) + keylist = [] + for key in [x for x in c.op_keylist_all(rcpt, 1)]: + valid = 0 + subkey = key.subkeys + while subkey: + keyid = subkey.keyid + if keyid == None: + break + can_encrypt = subkey.can_encrypt + valid += can_encrypt + subkey = subkey.next + if valid: + keylist.append(key) + if keylist: + c.set_armor(1) + c.op_encrypt_sign(keylist, 1, plain, sig) + else: + c.op_sign(plain, sig, mode.CLEAR) + sig.seek(0,0) + + msg = MIMEText(sig.read()) + msg['Subject'] = subject + msg['From'] = signer + msg['To'] = rcpt + + s = smtplib.SMTP() + s.connect() + s.sendmail(signer, [rcpt], msg.as_string()) + s.close() + 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 \ + sqlalchemy.object_mapper( + self.delegateto).local_table.columns \ if not col.primary_key and not col.nullable]: - if self.__getattribute__(key) is None: + if self.delegateto.__getattribute__(key) is None: missingfields.append(key) if missingfields: raise MissingFieldsError(missingfields) + + def write_to_file(self, filename, template): + """Write the data from template to the specified file.""" + tmp = tempfile.NamedTemporaryFile() + tmp.write(template) + tmp.flush() + cmd = 'cp "%s" "%s"' % (tmp.name, filename) + self.sucommand(cmd) + tmp.close() + diff --git a/gnuviechadmin/backend/BackendEntityHandler.py b/gnuviechadmin/backend/BackendEntityHandler.py index 3b3384b..32a83ae 100644 --- a/gnuviechadmin/backend/BackendEntityHandler.py +++ b/gnuviechadmin/backend/BackendEntityHandler.py @@ -19,51 +19,68 @@ # # Version: $Id$ -from sqlalchemy import * +import sqlalchemy, logging from gnuviechadmin.exceptions import * from BackendEntity import * class BackendEntityHandler(object): - def __init__(self, entityclass, verbose = False): + def __init__(self, entityclass, toclass, verbose = False): + self.logger = logging.getLogger("%s.%s" % ( + self.__class__.__module__, self.__class__.__name__)) self.entityclass = entityclass + self.toclass = toclass self.verbose = verbose def create(self, **kwargs): -# try: - sess = create_session() - entity = self.entityclass(self.verbose, **kwargs) - try: - entity.create_hook() - sess.save(entity) - sess.flush() - except: - sess.delete(entity) - sess.flush() - raise -# except Exception, e: -# raise CreationFailedError(self.entityclass.__name__, e) + """Create a new entity of the managed type with the fields set + to the values in kwargs.""" + self.logger.debug("create with params %s", str(kwargs)) + sess = sqlalchemy.create_session() + transaction = sess.create_transaction() + delegate = self.toclass(**kwargs) + entity = self.entityclass(delegate, self.verbose) + try: + sess.save(delegate) + sess.flush() + sess.refresh(delegate) + entity.create_hook(sess) + sess.flush() + transaction.commit() + except: + transaction.rollback() + self.logger.exception("Exception in create.") + raise - def fetchall(self): + def fetchall(self, **kwargs): """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 + self.logger.debug("fetchall with params %s", str(kwargs)) + session = sqlalchemy.create_session() + query = session.query(self.toclass) + if kwargs: + allentities = query.select_by(**kwargs) + else: + allentities = query.select() + return [self.entityclass(entity, self.verbose) \ + for entity in allentities] def delete(self, pkvalue): """Deletes the entity of the managed entity type that has the specified primary key value.""" + self.logger.debug("delete with primary key %s", str(pkvalue)) + sess = sqlalchemy.create_session() + transaction = sess.create_transaction() try: - sess = create_session() - entity = sess.query(self.entityclass).get(pkvalue) - if entity: - BackendEntity.__init__(entity, self.verbose) + to = sess.query(self.toclass).get(pkvalue) + if to: + entity = self.entityclass(to, self.verbose) + self.logger.info("delete %s", str(entity)) if self.verbose: print "delete %s" % (str(entity)) - entity.delete_hook() - sess.delete(entity) + entity.delete_hook(sess) + sess.delete(to) sess.flush() + transaction.commit() except Exception, e: - raise DeleteFailedError(self.entityclass.__name__, e) + transaction.rollback() + self.logger.exception("Exception in delete.") + raise diff --git a/gnuviechadmin/backend/BackendTo.py b/gnuviechadmin/backend/BackendTo.py new file mode 100644 index 0000000..397da3b --- /dev/null +++ b/gnuviechadmin/backend/BackendTo.py @@ -0,0 +1,73 @@ +# -*- 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 object_mapper + +from tables import * + +class BackendTo(object): + """Backend transfer object class.""" + def __init__(self, **kwargs): + for (key, value) in kwargs.items(): + self.__setattr__(key, value) + + def __repr__(self, **kwargs): + if 'verbose' in kwargs and kwargs['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 + +class Client(BackendTo): + """Transfer object class for clients.""" + _shortkeys = ('clientid', 'firstname', 'lastname', 'email') + +class Sysuser(BackendTo): + """Transfer object class for system users.""" + _shortkeys = ("sysuserid", "clientid", "username", "home", "shell") + +class Domain(BackendTo): + """Transfer object class for DNS domains.""" + _shortkeys = ("domainid", "sysuserid", "name", "type") + +class Record(BackendTo): + """Transfer object class for DNS domain records.""" + _shortkeys = ("recordid", "domainid", "name", "type", "content") + +client_mapper = mapper(Client, client_table) +sysuser_mapper = mapper(Sysuser, sysuser_table) +domain_mapper = mapper(Domain, domain_table) +record_mapper = mapper(Record, record_table) + +client_mapper.add_property("sysusers", relation(Sysuser, backref = 'client')) + +sysuser_mapper.add_property("domains", relation(Domain, backref = 'sysuser')) + +domain_mapper.add_property("records", relation(Record, cascade = 'all', + backref = 'domain')) diff --git a/gnuviechadmin/backend/client.py b/gnuviechadmin/backend/client.py index a310fe4..6af57db 100644 --- a/gnuviechadmin/backend/client.py +++ b/gnuviechadmin/backend/client.py @@ -19,42 +19,58 @@ # # Version: $Id$ -from sqlalchemy import * -from tables import client_table from gnuviechadmin.exceptions import * - -import sysuser +from settings import config +from BackendTo import * from BackendEntity import * from BackendEntityHandler import * -class Client(BackendEntity): +class ClientEntity(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 + def __init__(self, delegate, verbose = False, **kwargs): + BackendEntity.__init__(self, delegate, verbose) for (key, value) in kwargs.items(): self.__setattr__(key, value) + if not self.delegateto.country: + self.delegateto.country = self._get_default_country() self.validate() - def create_hook(self): - pass + def _client_mail(self): + text = get_template(config.get('common', 'mailtemplates'), + config.get('client', 'create.mail')).substitute({ + 'firstname' : self.delegateto.firstname, + 'lastname' : self.delegateto.lastname, + 'email' : self.delegateto.email, + 'address1' : self.delegateto.address1, + 'zipcode' : self.delegateto.zip, + 'city' : self.delegateto.city, + 'phone' : self.delegateto.phone}) + subject = get_template_string( + config.get('client', 'create_subject')).substitute({ + 'firstname' : self.delegateto.firstname, + 'lastname' : self.delegateto.lastname}) + self.send_mail(subject, text) - def delete_hook(self): - pass + def create_hook(self, session): + """Actions to perform when a client is created.""" + self._client_mail() -client_mapper = mapper(Client, client_table) -client_mapper.add_property("sysusers", relation(sysuser.Sysuser)) + def delete_hook(self, session): + """Actions to perform when a client is deleted.""" + if self.delegateto.sysusers: + raise CannotDeleteError( + self.delegateto, + "it still has the following system users assigned: %s" % ( + ", ".join([sysuser.username for sysuser in \ + self.delegateto.sysusers]))) + + def _get_default_country(self): + """Gets the default country.""" + return config.get('common', 'defaultcountry') class ClientHandler(BackendEntityHandler): """BackendEntityHandler for Client entities.""" def __init__(self, verbose = False): - BackendEntityHandler.__init__(self, Client, verbose) + BackendEntityHandler.__init__(self, ClientEntity, Client, verbose) diff --git a/gnuviechadmin/backend/domain.py b/gnuviechadmin/backend/domain.py index bf7681e..2935762 100644 --- a/gnuviechadmin/backend/domain.py +++ b/gnuviechadmin/backend/domain.py @@ -19,37 +19,29 @@ # # Version: $Id: client.py 1101 2007-02-28 21:15:20Z jan $ -from sqlalchemy import * -from tables import domain_table +import datetime, os + from gnuviechadmin.exceptions import * +from settings import * +from BackendTo import Record, Domain +from BackendEntity import BackendEntity +from BackendEntityHandler import BackendEntityHandler -from record import Record -import datetime -from BackendEntity import * -from BackendEntityHandler import * -from settings import config - -class Domain(BackendEntity): +class DomainEntity(BackendEntity): """Entity class for DNS domains.""" - _shortkeys = ("domainid", "sysuserid", "name", "type") _valid_domain_types = ("MASTER", "SLAVE") - def __init__(self, verbose = False, **kwargs): - BackendEntity.__init__(self, verbose) - self.domainid = None - self.sysuserid = None - self.name = None - self.type = None - self.master = None + def __init__(self, delegate, verbose = False, **kwargs): + BackendEntity.__init__(self, delegate, verbose) self.ns1 = None self.ns2 = None self.mx = None self.ipaddr = None for (key, value) in kwargs.items(): self.__setattr__(key, value) - if not self.type: - self.type = self.getdefaultdomaintype() + if not self.delegateto.type: + self.delegateto.type = self.getdefaultdomaintype() if not self.ns1: self.ns1 = config.get('domain', 'defaultns1') if not self.ns2: @@ -58,7 +50,7 @@ class Domain(BackendEntity): self.mx = config.get('domain', 'defaultmx') if not self.ipaddr: self.ipaddr = config.get('domain', 'defaultip') - self.type = self.type.upper() + self.delegateto.type = self.delegateto.type.upper() self.validate() def getdefaultdomaintype(self): @@ -66,13 +58,13 @@ class Domain(BackendEntity): def validate(self): BackendEntity.validate(self) - if not self.type in self._valid_domain_types: + if not self.delegateto.type in self._valid_domain_types: raise ValidationFailedError( - self, "invalid domain type %s" % (self.type)) - if self.type == 'SLAVE' and not self.master: + self, "invalid domain type %s" % (self.delegateto.type)) + if self.delegateto.type == 'SLAVE' and not self.delegateto.master: raise ValidationFailedError( self, "you have to specify a master for slave domains.") - if self.type == 'MASTER': + if self.delegateto.type == 'MASTER': if not self.ns1 or not self.ns2: raise ValidationFailedError( self, "two nameservers must be specified.") @@ -80,10 +72,17 @@ class Domain(BackendEntity): raise ValidationFailedError( self, "a primary mx host must be specified.") - def _getnewserial(self): + def _getnewserial(self, oldserial = None): current = datetime.datetime.now() - return int("%04d%02d%02d01" % \ - (current.year, current.month, current.day)) + datepart = "%04d%02d%02d" % \ + (current.year, current.month, current.day) + retval = None + if oldserial: + if str(oldserial)[:len(datepart)] == datepart: + retval = oldserial + 1 + if not retval: + retval = int("%s01" % (datepart)) + return retval def _getnewsoa(self): return '%s %s %d %d %d %d %d' % \ @@ -95,36 +94,228 @@ class Domain(BackendEntity): config.getint('domain', 'defaultexpire'), config.getint('domain', 'defaultminimumttl')) - def create_hook(self): - self.records.append(Record( - name = self.name, type = 'SOA', + def update_serial(self, session): + query = session.query(Record) + soarecord = query.get_by(Record.c.type == 'SOA', + Record.c.domainid == self.delegateto.domainid) + parts = soarecord.content.split(" ") + parts[2] = str(self._getnewserial(int(parts[2]))) + soarecord.content = " ".join(parts) + session.save(soarecord) + session.flush() + + def _get_vhost_dir(self): + return os.path.join(self.delegateto.sysuser.home, + self.delegateto.name, + config.get('domain', 'htdir')) + + def _get_log_dir(self): + return os.path.join(config.get('domain', 'logpath'), + self.delegateto.name) + + def _get_stats_dir(self): + return os.path.join(config.get('domain', 'statspath'), + self.delegateto.name) + + def _create_vhost_dir(self): + vhostdir = self._get_vhost_dir() + cmd = 'mkdir -p "%s"' % (vhostdir) + self.sucommand(cmd) + for tpl in [tpl for tpl in os.listdir( + get_template_dir(config.get('domain', 'htdocstemplate'))) \ + if not tpl.startswith('.')]: + template = get_template(config.get('domain', 'htdocstemplate'), + tpl) + template = template.substitute({ + 'domain' : self.delegateto.name}) + self.write_to_file(os.path.join(vhostdir, tpl), template) + cmd = 'chown -R %(username)s:%(group)s "%(dir)s"' % { + 'username' : self.delegateto.sysuser.username, + 'group' : config.get('sysuser', 'defaultgroup'), + 'dir' : vhostdir} + self.sucommand(cmd) + + def _create_log_dir(self): + cmd = 'mkdir -p "%s"' % (self._get_log_dir()) + self.sucommand(cmd) + + def _get_auth_userfile(self): + authdir = config.get('domain', 'authdir') + if not os.path.isdir(authdir): + cmd = 'mkdir -p "%s"' % (authdir) + self.sucommand(cmd) + return os.path.join(authdir, '%s.passwd' % (self.delegateto.name)) + + def _create_stats_dir(self): + """Creates the statistics directory for the domain and sets + Apache .htaccess password protection.""" + statsdir = self._get_stats_dir() + authfile = self._get_auth_userfile() + cmd = 'htpasswd -m -c -b "%s" "%s" "%s"' % ( + authfile, + self.delegateto.sysuser.username, + self.delegateto.sysuser.clearpass) + self.sucommand(cmd) + cmd = 'mkdir -p "%s"' % (statsdir) + self.sucommand(cmd) + template = get_template(config.get('domain', 'conftemplates'), + config.get('domain', 'statshtaccesstemplate')) + template = template.substitute({ + 'domain' : self.delegateto.name, + 'userfile' : authfile}) + self.write_to_file(os.path.join(self._get_stats_dir(), + '.htaccess'), template) + + def _create_stats_conf(self): + modlogandir = os.path.join(config.get('domain', + 'modlogandir'), + self.delegateto.sysuser.username) + cmd = 'mkdir -p "%s"' % (modlogandir) + self.sucommand(cmd) + template = get_template(config.get('domain', 'conftemplates'), + config.get('domain', 'modlogantemplate')) + template = template.substitute({ + 'statsdir' : self._get_stats_dir(), + 'logdir' : self._get_log_dir(), + 'domain' : self.delegateto.name, + 'domainesc' : self.delegateto.name.replace('.', '\.')}) + self.write_to_file(os.path.join(modlogandir, + self.delegateto.name + '.conf'), + template) + + def _create_apache_conf(self): + template = get_template(config.get('domain', 'conftemplates'), + config.get('domain', 'apachetemplate')) + template = template.substitute({ + 'ipaddr' : self.ipaddr, + 'statsdir' : self._get_stats_dir(), + 'logdir' : self._get_log_dir(), + 'domain' : self.delegateto.name, + 'docroot' : self._get_vhost_dir()}) + self.write_to_file(os.path.join(config.get('domain', 'sitesdir'), + self.delegateto.name), template) + + def _mail_domain(self): + template = get_template(config.get('common', 'mailtemplates'), + config.get('domain', 'create.mail')) + text = template.substitute({ + 'sysuser' : self.delegateto.sysuser.username, + 'domain' : self.delegateto.name, + 'docroot' : self._get_vhost_dir(), + 'statspass' : self.delegateto.sysuser.clearpass}) + template = get_template_string(config.get('domain', 'create_subject')) + subject = template.substitute({ + 'domain' : self.delegateto.name}) + self.send_mail(subject, text) + + def create_hook(self, session): + self.delegateto.records.append(Record( + name = self.delegateto.name, type = 'SOA', content = self._getnewsoa(), ttl = config.getint('domain', 'defaultttl'))) - self.records.append(Record( - name = self.name, type = 'NS', content = self.ns1, + self.delegateto.records.append(Record( + name = self.delegateto.name, type = 'NS', content = self.ns1, ttl = config.getint('domain', 'defaultttl'))) - self.records.append(Record( - name = self.name, type = 'NS', content = self.ns2, + self.delegateto.records.append(Record( + name = self.delegateto.name, type = 'NS', content = self.ns2, ttl = config.getint('domain', 'defaultttl'))) - self.records.append(Record( - name = self.name, type = 'MX', content = self.mx, + self.delegateto.records.append(Record( + name = self.delegateto.name, type = 'MX', content = self.mx, ttl = config.getint('domain', 'defaultttl'), prio = config.getint('domain', 'defaultmxprio'))) - self.records.append(Record( - name = self.name, type = 'A', content = self.ipaddr, + self.delegateto.records.append(Record( + name = self.delegateto.name, type = 'A', content = self.ipaddr, ttl = config.getint('domain', 'defaultttl'))) - self.records.append(Record( - name = "www.%s" % (self.name), type = 'A', content = self.ipaddr, + self.delegateto.records.append(Record( + name = "www.%s" % (self.delegateto.name), type = 'A', + content = self.ipaddr, ttl = config.getint('domain', 'defaultttl'))) + session.save(self.delegateto) + session.flush() + self._create_vhost_dir() + self._create_log_dir() + self._create_stats_dir() + self._create_stats_conf() + self._create_apache_conf() + self._mail_domain() - def delete_hook(self): - pass + def _delete_apache_conf(self): + cmd = 'a2dissite %s' % (self.delegateto.name) + self.sucommand(cmd) + cmd = 'rm "%s"' % (os.path.join(config.get('domain', 'sitesdir'), + self.delegateto.name)) + self.sucommand(cmd) -domain_mapper = mapper(Domain, domain_table) -domain_mapper.add_property("records", relation(Record)) + def _delete_stats_conf(self): + cmd = 'rm "%s"' % (os.path.join(config.get('domain', 'modlogandir'), + self.delegateto.sysuser.username, + self.delegateto.name + '.conf')) + self.sucommand(cmd) + + def _archive_stats_dir(self): + archive = os.path.join(self.delegateto.sysuser.home, + '%(domain)s-stats.tar.gz' % { + 'domain' : self.delegateto.name}) + cmd = 'tar czf "%(archive)s" --directory="%(statsbase)s" "%(statsdir)s"' % { + 'archive' : archive, + 'statsbase' : config.get('domain', 'statspath'), + 'statsdir' : self.delegateto.name} + self.sucommand(cmd) + cmd = 'rm -r "%(statsdir)s"' % { + 'statsdir' : self._get_stats_dir()} + self.sucommand(cmd) + cmd = 'chown "%(username)s:%(group)s" "%(archive)s"' % { + 'username' : self.delegateto.sysuser.username, + 'group' : config.get('sysuser', 'defaultgroup'), + 'archive' : archive} + self.sucommand(cmd) + + def _archive_log_dir(self): + archive = os.path.join(self.delegateto.sysuser.home, + '%(domain)s-logs.tar.gz' % { + 'domain' : self.delegateto.name}) + cmd = 'tar czf "%(archive)s" --directory="%(logbase)s" "%(logdir)s"' % { + 'archive' : archive, + 'logbase' : config.get('domain', 'logpath'), + 'logdir' : self.delegateto.name} + self.sucommand(cmd) + cmd = 'rm -r "%(logdir)s"' % { + 'logdir' : self._get_log_dir()} + self.sucommand(cmd) + cmd = 'chown "%(username)s:%(group)s" "%(archive)s"' % { + 'username' : self.delegateto.sysuser.username, + 'group' : config.get('sysuser', 'defaultgroup'), + 'archive' : archive} + self.sucommand(cmd) + + def _archive_vhost_dir(self): + archive = os.path.join(self.delegateto.sysuser.home, + '%(domain)s-vhost.tar.gz' % { + 'domain' : self.delegateto.name}) + cmd = 'tar czf "%(archive)s" --directory="%(vhostbase)s" "%(vhostdir)s"' % { + 'archive' : archive, + 'vhostbase' : self.delegateto.sysuser.home, + 'vhostdir' : self.delegateto.name} + self.sucommand(cmd) + cmd = 'rm -r "%(vhostdir)s"' % { + 'vhostdir' : os.path.join(self.delegateto.sysuser.home, + self.delegateto.name)} + self.sucommand(cmd) + cmd = 'chown "%(username)s:%(group)s" "%(archive)s"' % { + 'username' : self.delegateto.sysuser.username, + 'group' : config.get('sysuser', 'defaultgroup'), + 'archive' : archive} + self.sucommand(cmd) + + def delete_hook(self, session): + self._delete_apache_conf() + self._delete_stats_conf() + self._archive_stats_dir() + self._archive_log_dir() + self._archive_vhost_dir() class DomainHandler(BackendEntityHandler): """BackendEntityHandler for Domain entities.""" def __init__(self, verbose = False): - BackendEntityHandler.__init__(self, Domain, verbose) + BackendEntityHandler.__init__(self, DomainEntity, Domain, verbose) diff --git a/gnuviechadmin/backend/record.py b/gnuviechadmin/backend/record.py index 7608428..93b023e 100644 --- a/gnuviechadmin/backend/record.py +++ b/gnuviechadmin/backend/record.py @@ -19,37 +19,24 @@ # # Version: $Id$ -from sqlalchemy import * -from tables import record_table from gnuviechadmin.exceptions import * - +from BackendTo import Record, Domain +from domain import DomainEntity from BackendEntity import * from BackendEntityHandler import * -class Record(object): +class RecordEntity(BackendEntity): """Entity class for DNS domain records.""" - def __init__(self, **kwargs): - for (key, value) in kwargs.items(): - self.__setattr__(key, value) - - #_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) + def create_hook(self, session): + domain = session.load(Domain, self.delegateto.domainid) + DomainEntity(domain).update_serial(session) + + def delete_hook(self, session): + domain = session.load(Domain, self.delegateto.domainid) + DomainEntity(domain).update_serial(session) class RecordHandler(BackendEntityHandler): """BackendEntityHandler for Record entities.""" def __init__(self, verbose = False): - BackendEntityHandler.__init__(self, Record, verbose) + BackendEntityHandler.__init__(self, RecordEntity, Record, verbose) diff --git a/gnuviechadmin/backend/settings.py b/gnuviechadmin/backend/settings.py index cb11513..afac9fe 100644 --- a/gnuviechadmin/backend/settings.py +++ b/gnuviechadmin/backend/settings.py @@ -19,13 +19,28 @@ # # Version: $Id$ -import ConfigParser, os +import ConfigParser, os, string, logging.config # global settings which must not be user configurable -required_version = 2 +required_version = 3 dbschema = 'gva' # load user configuration config = ConfigParser.ConfigParser() config.readfp(open('gnuviechadmin/defaults.cfg')) config.read(['gnuviechadmin/gva.cfg', os.path.expanduser('~/.gva.cfg')]) + +def get_template_dir(dirname): + """Returns the template directory for the given directory.""" + templatepath = config.get('common', 'templatedir') + return os.path.join(templatepath, dirname) + +def get_template(dirname, filename): + """Returns the template data from the given template file.""" + templatefile = file(os.path.join(get_template_dir(dirname), + filename)) + return string.Template(templatefile.read()) + +def get_template_string(templatestring): + """Returns a template object for the given template string.""" + return string.Template(templatestring) diff --git a/gnuviechadmin/backend/sysuser.py b/gnuviechadmin/backend/sysuser.py index fd625bd..6c1c4e4 100644 --- a/gnuviechadmin/backend/sysuser.py +++ b/gnuviechadmin/backend/sysuser.py @@ -19,120 +19,123 @@ # # Version: $Id$ -from sqlalchemy import * -from tables import sysuser_table from gnuviechadmin.exceptions import * from gnuviechadmin.util import passwordutils, getenttools - +from settings import config +from BackendTo import * from BackendEntity import * from BackendEntityHandler import * -class Sysuser(BackendEntity): +class SysuserEntity(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 + def __init__(self, delegate, verbose = False, **kwargs): + BackendEntity.__init__(self, delegate, verbose) for (key, value) in kwargs.items(): self.__setattr__(key, value) - 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() + if not self.delegateto.username: + self.delegateto.username = self._get_next_sysusername() + if not self.delegateto.usertype: + self.delegateto.usertype = self._get_default_sysusertype() + if not self.delegateto.home: + self.delegateto.home = self._get_home(self.delegateto.username) + if not self.delegateto.shell: + self.delegateto.shell = self._get_default_shell() + (self.delegateto.clearpass, self.delegateto.md5pass) = \ + passwordutils.get_pw_tuple(self.delegateto.clearpass) + if not self.delegateto.sysuid: + self.delegateto.sysuid = self._get_next_sysuid() self.validate() - def getnextsysusername(self): - prefix = self.config.get('sysuser', 'nameprefix') + def _get_next_sysusername(self): + prefix = config.get('sysuser', 'nameprefix') usernames = [user.username for user in \ - getenttools.finduserbyprefix(prefix)] + getenttools.find_user_by_prefix(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): + def _get_default_sysusertype(self): return 1 - def gethome(self, sysusername): + def _get_home(self, sysusername): """Gets a valid home directory for the given user name.""" - return os.path.join(self.config.get('sysuser', 'homedirbase'), + return os.path.join(config.get('sysuser', 'homedirbase'), sysusername) - def getdefaultshell(self): + def _get_default_shell(self): return False - def getshellbinary(self): - if self.shell: - return self.config.get('sysuser', 'shellyes') - return self.config.get('sysuser', 'shellno') + def _get_shell_binary(self): + if self.delegateto.shell: + return config.get('sysuser', 'shellyes') + return config.get('sysuser', 'shellno') - def getnextsysuid(self): - uid = int(self.config.get('sysuser', 'minuid')) - muid = getenttools.getmaxuid(int(self.config.get('sysuser', - 'maxuid'))) + def _get_next_sysuid(self): + uid = int(config.get('sysuser', 'minuid')) + muid = getenttools.get_max_uid(int(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) + def _populate_home(self): + templatedir = get_template_dir(config.get('sysuser', 'hometemplate')) cmdline = 'cp -R "%(template)s" "%(home)s"' % { 'template' : templatedir, - 'home' : self.home } + 'home' : self.delegateto.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 } + 'username' : self.delegateto.username, + 'group' : config.get('sysuser', 'defaultgroup'), + 'home' : self.delegateto.home } self.sucommand(cmdline) - def create_hook(self): - gecos = self.config.get('sysuser', 'gecos') - gecos = gecos % (self.username) + def _mail_sysuser(self): + template = get_template(config.get('common', 'mailtemplates'), + config.get('sysuser', 'create.mail')) + text = template.substitute({ + 'uid' : self.delegateto.sysuid, + 'firstname' : self.delegateto.client.firstname, + 'lastname' : self.delegateto.client.lastname, + 'email' : self.delegateto.client.email, + 'username' : self.delegateto.username, + 'password' : self.delegateto.clearpass, + 'home' : self.delegateto.home, + 'shell' : self._get_shell_binary()}) + template = get_template_string(config.get('sysuser', 'create_subject')) + subject = template.substitute({ + 'username' : self.delegateto.username}) + self.send_mail(subject, text) + + def create_hook(self, session): + gecos = config.get('sysuser', 'gecos') % (self.delegateto.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'), + 'home' : self.delegateto.home, + 'shell' : self._get_shell_binary(), + 'sysuid' : self.delegateto.sysuid, + 'group' : config.get('sysuser', 'defaultgroup'), 'gecos' : gecos, - 'username' : self.username} + 'username' : self.delegateto.username} self.sucommand(cmdline) cmdline = 'chpasswd --encrypted' inline = '%(username)s:%(md5pass)s' % { - 'username' : self.username, - 'md5pass' : self.md5pass} + 'username' : self.delegateto.username, + 'md5pass' : self.delegateto.md5pass} self.sucommand(cmdline, inline) - self.populate_home() + self._populate_home() + self._mail_sysuser() - def delete_hook(self): - backupdir = os.path.join(self.config.get('common', - 'backupdir'), - self.config.get('sysuser', - 'homebackupdir')) + def delete_hook(self, session): + if self.delegateto.domains: + raise CannotDeleteError( + self.delegateto, + "it still has the following domains assigned: %s" % ( + ", ".join([domain.name for domain in \ + self.delegateto.domains]))) + backupdir = os.path.join(config.get('common', 'backupdir'), + config.get('sysuser', 'homebackupdir')) if not os.path.isdir(backupdir): cmdline = 'mkdir -p "%(backupdir)s"' % { 'backupdir' : backupdir} @@ -141,13 +144,11 @@ class Sysuser(BackendEntity): raise Exception("could not create backup directory") cmdline = 'deluser --remove-home --backup --backup-to "%(backupdir)s" %(username)s' % { 'backupdir' : backupdir, - 'username' : self.username} + 'username' : self.delegateto.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) + BackendEntityHandler.__init__(self, SysuserEntity, Sysuser, verbose) diff --git a/gnuviechadmin/backend/tables.py b/gnuviechadmin/backend/tables.py index a79192a..b8d314d 100644 --- a/gnuviechadmin/backend/tables.py +++ b/gnuviechadmin/backend/tables.py @@ -24,82 +24,49 @@ import sys import migrate.versioning.api from settings import * -dbversion = migrate.versioning.api.db_version( - config.get('database', 'uri'), - config.get('database', 'repository')) -if dbversion < required_version: - print("""Database version is %d but required version is %d, run +try: + dbversion = migrate.versioning.api.db_version( + config.get('database', 'uri'), + config.get('database', 'repository')) + if dbversion < required_version: + print("""Database version is %d but required version is %d, run migrate upgrade %s %s to fix this.""" % - (dbversion, required_version, config.get('database', 'uri'), - config.get('database', 'repository'))) + (dbversion, required_version, config.get('database', 'uri'), + config.get('database', 'repository'))) + sys.exit(1) +except exceptions.NoSuchTableError, nste: + print nste sys.exit(1) meta = BoundMetaData(config.get('database', 'uri')) +#meta.engine.echo = True client_table = Table( - 'client', meta, - Column('clientid', Integer, primary_key=True), - Column('title', String(10)), - Column('firstname', String(64), nullable=False), - Column('lastname', String(64), nullable=False), - Column('address1', String(64), nullable=False), - Column('address2', String(64)), - Column('zip', String(7), nullable=False), - Column('city', String(64), nullable=False), - Column('country', String(5), nullable=False), - Column('phone', String(32), nullable=False), - Column('mobile', String(32)), - Column('fax', String(32)), - Column('email', String(64), unique=True, nullable=False), - schema = dbschema - ) + 'client', meta, schema = dbschema, autoload = True) sysuser_table = Table( - 'sysuser', meta, - Column('sysuserid', Integer, primary_key=True), - Column('username', String(12), nullable=False, unique=True), - Column('usertype', Integer, nullable=False, default=0, index=True), - Column('home', String(128)), - Column('shell', Boolean, nullable=False, default=False), - Column('clearpass', String(64)), - 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()), - schema = dbschema - ) + 'sysuser', meta, ForeignKeyConstraint(['clientid'], ['client.clientid']), + schema = dbschema, autoload = 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), - schema = dbschema - ) + 'domain', meta, ForeignKeyConstraint(['sysuserid'], ['sysuser.sysuserid']), + schema = dbschema, autoload = 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), - schema = dbschema - ) + 'record', meta, ForeignKeyConstraint(['domainid'], ['domain.domainid']), + schema = dbschema, autoload = 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), - schema = dbschema - ) + ForeignKeyConstraint(['account'], ['sysuser.sysuserid']), + schema = dbschema, autoload = True) +mailaccount_table = Table( + 'mailaccount', meta, + ForeignKeyConstraint(['domainid'], ['domain.domainid']), + schema = dbschema, autoload = True) +mailaddress_table = Table( + 'mailaddress', meta, + ForeignKeyConstraint(['domainid'], ['domain.domainid']), + schema = dbschema, autoload = True) +mailtarget_table = Table( + 'mailtarget', meta, + ForeignKeyConstraint(['mailaddressid'], ['mailaddress.mailaddressid']), + schema = dbschema, autoload = True) diff --git a/gnuviechadmin/cli/CliCommand.py b/gnuviechadmin/cli/CliCommand.py index fcc3646..7cabc29 100644 --- a/gnuviechadmin/cli/CliCommand.py +++ b/gnuviechadmin/cli/CliCommand.py @@ -19,7 +19,7 @@ # # Version: $Id$ -import getopt, sys +import getopt, sys, logging from gnuviechadmin.exceptions import GnuviechadminError class CliCommand: @@ -198,6 +198,8 @@ Common options: def __init__(self, args): """This initializes the command with the given command line arguments and executes it.""" + self.logger = logging.getLogger("%s.%s" % ( + self.__class__.__module__, self.__class__.__name__)) self._data = {} if len(args) > 0: if args[0] in self._subcommands(): diff --git a/gnuviechadmin/cli/client.py b/gnuviechadmin/cli/client.py index ec26ff7..0c46261 100644 --- a/gnuviechadmin/cli/client.py +++ b/gnuviechadmin/cli/client.py @@ -59,6 +59,8 @@ class ClientCli(CliCommand.CliCommand): "the client id", True)])} def _execute(self, subcommand): + self.logger.debug("execute %s with data %s", subcommand, + str(self._data)) from gnuviechadmin.backend import client from gnuviechadmin import exceptions if subcommand == "create": diff --git a/gnuviechadmin/cli/domain.py b/gnuviechadmin/cli/domain.py index da823f5..40e7888 100644 --- a/gnuviechadmin/cli/domain.py +++ b/gnuviechadmin/cli/domain.py @@ -43,6 +43,8 @@ class DomainCli(CliCommand.CliCommand): "the domain id", True)])} def _execute(self, subcommand): + self.logger.debug("execute %s with data %s", subcommand, + str(self._data)) from gnuviechadmin.backend.domain import DomainHandler from gnuviechadmin import exceptions if subcommand == "create": diff --git a/gnuviechadmin/cli/record.py b/gnuviechadmin/cli/record.py index efe8a99..3b07c12 100644 --- a/gnuviechadmin/cli/record.py +++ b/gnuviechadmin/cli/record.py @@ -37,16 +37,19 @@ class RecordCli(CliCommand.CliCommand): (["-p", "--prio"], "prio", "MX record priority", False), (["--ttl"], "ttl", - "Time to live", False), + "time to live", False), (["-d", "--domainid"], "domainid", - "Domain id", True)]), + "domain id", True)]), 'list' : ("lists existing records", - []), + [(["-d", "--domainid"], "domainid", + "domain id", False)]), 'delete' : ("delete a record", [(["-r", "--recordid"], "recordid", "the record id", True)])} def _execute(self, subcommand): + self.logger.debug("execute %s with data %s", subcommand, + str(self._data)) from gnuviechadmin.backend.record import RecordHandler from gnuviechadmin import exceptions if subcommand == "create": @@ -60,7 +63,7 @@ class RecordCli(CliCommand.CliCommand): print cfe sys.exit(2) elif subcommand == "list": - records = RecordHandler(self._verbose).fetchall() + records = RecordHandler(self._verbose).fetchall(**self._data) for record in records: print record elif subcommand == "delete": diff --git a/gnuviechadmin/cli/sysuser.py b/gnuviechadmin/cli/sysuser.py index 27ac177..88719cd 100644 --- a/gnuviechadmin/cli/sysuser.py +++ b/gnuviechadmin/cli/sysuser.py @@ -47,6 +47,8 @@ class SysuserCli(CliCommand.CliCommand): "the system user id", True)])} def _execute(self, subcommand): + self.logger.debug("execute %s with data %s", subcommand, + str(self._data)) from gnuviechadmin.backend import sysuser from gnuviechadmin import exceptions if subcommand == "create": diff --git a/gnuviechadmin/defaults.cfg b/gnuviechadmin/defaults.cfg index a065003..2105156 100644 --- a/gnuviechadmin/defaults.cfg +++ b/gnuviechadmin/defaults.cfg @@ -35,6 +35,13 @@ repository = /etc/gnuviechadmin/dbrepo [common] suwrapper = sudo backupdir = /var/backups/gnuviechadmin +templatedir = /etc/gnuviechadmin/templates +mailtemplates = mails +log.cfg = /etc/gnuviechadmin/logging.cfg + +[client] +create.mail = create_client.txt +create_subject = A new client ${firstname} ${lastname} has been created. [sysuser] nameprefix = usr @@ -46,4 +53,27 @@ shellno = /usr/bin/scponly defaultgroup = wwwusers gecos = Webuser %s homebackupdir = homes -hometemplate = /etc/gnuviechadmin/templates/home +hometemplate = home +create.mail = create_sysuser.txt +create_subject = A new system user ${username} has been created. + +[domain] +defaultmxprio = 5 +defaultrefresh = 86400 +defaultretry = 7200 +defaultexpire = 1209600 +defaultminimumttl = 86400 +defaultttl = 86400 +htdir = html +logpath = /var/log/apache2 +statspath = /home/stats +htdocstemplate = domain +conftemplates = domainconf +apachetemplate = vhost.conf +statshtaccesstemplate = htaccess-stats +modlogantemplate = modlogan.conf +modlogandir = /var/lib/gnuviechadmin/stats +sitesdir = /etc/apache2/sites-available +authdir = /etc/apache2/authdata +create.mail = create_domain.txt +create_subject = A new domain ${domain} has been created. diff --git a/gnuviechadmin/exceptions.py b/gnuviechadmin/exceptions.py index 10a0ff7..ec0d290 100644 --- a/gnuviechadmin/exceptions.py +++ b/gnuviechadmin/exceptions.py @@ -73,3 +73,16 @@ class ValidationFailedError(GnuviechadminError): if self.cause: msg += " The reason is %s." % (str(self.cause)) return msg + +class CannotDeleteError(GnuviechadminError): + """This exception should be raised if an entity cannot be deleted + because of some unmatched precondition.""" + def __init__(self, instance, cause = None): + self.instance = instance + self.cause = cause + + def __str__(self): + msg = "Cannot delete %s." % (str(self.instance)) + if self.cause: + msg += " The reason is %s." % (str(self.cause)) + return msg diff --git a/gnuviechadmin/logging.cfg b/gnuviechadmin/logging.cfg new file mode 100644 index 0000000..a0cdfa7 --- /dev/null +++ b/gnuviechadmin/logging.cfg @@ -0,0 +1,34 @@ +[formatters] +keys=simple + +[formatter_simple] +format=%(asctime)s %(levelname)s %(name)s: %(message)s +datefmt= + +[handlers] +keys=handler01,handler02 + +[handler_handler01] +class=handlers.RotatingFileHandler +level=DEBUG +formatter=simple +args=('gnuviechadmin.log', 'a', 10485760, 5) + +[handler_handler02] +class=StreamHandler +level=NOTSET +formatter=simple +args=(sys.stdout,) + +[loggers] +keys=root,gnuviechadmin + +[logger_root] +level=NOTSET +handlers=handler02 + +[logger_gnuviechadmin] +level=DEBUG +handlers=handler01 +propagate=0 +qualname=gnuviechadmin \ No newline at end of file diff --git a/gnuviechadmin/util/getenttools.py b/gnuviechadmin/util/getenttools.py index be3ed1f..ffa068e 100644 --- a/gnuviechadmin/util/getenttools.py +++ b/gnuviechadmin/util/getenttools.py @@ -19,7 +19,7 @@ # # Version: $Id$ -import os, popen2 +import pwd, grp class PasswdUser(object): """This class represents users in the user database.""" @@ -45,7 +45,7 @@ class PasswdGroup(object): def __init__(self, groupname, pw, gid, members): self.groupname = groupname self.gid = int(gid) - self.members = members.split(",") + self.members = members def __repr__(self): return "%s(%s:%d:%s)" % (self.__class__.__name__, @@ -53,42 +53,41 @@ class PasswdGroup(object): 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 parse_groups(): + return [PasswdGroup(*arr) for arr in grp.getgrall()] -def parseusers(): - (stdout, stdin) = popen2.popen2("getent passwd") - return [PasswdUser(*arr) for arr in [line.strip().split(":") for line in stdout]] +def parse_users(): + return [PasswdUser(*arr) for arr in pwd.getpwall()] -def finduserbyprefix(prefix): +def find_user_by_prefix(prefix): """Finds all user entries with the given prefix.""" - return [user for user in parseusers() if user.username.startswith(prefix)] + return [user for user in parse_users() if user.username.startswith(prefix)] -def getuserbyid(uid): +def get_user_by_id(uid): """Gets the user with the given user id.""" - users = [user for user in parseusers() if user.uid == uid] + users = [user for user in parse_users() if user.uid == uid] if users: return users[0] return None -def getgroupbyid(gid): +def get_group_by_id(gid): """Gets the group with the given group id.""" - groups = [group for group in parsegroups() if group.gid == gid] + groups = [group for group in parse_groups() if group.gid == gid] if groups: return groups[0] return None -def getmaxuid(boundary = 65536): +def get_max_uid(boundary = 65536): """Gets the highest uid value.""" - return max([user.uid for user in parseusers() if user.uid <= boundary]) + return max([user.uid for user in parse_users() if user.uid <= boundary]) -def getmaxgid(boundary = 65536): +def get_max_gid(boundary = 65536): """Gets the highest gid value.""" - return max([group.gid for group in parsegroups() if group.gid <= boundary]) + return max([group.gid for group in parse_groups() \ + 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))) + print "Max UID is %d" % (get_max_uid(40000)) + print "Max GID is %d" % (get_max_gid(40000)) + print "User with max UID is %s" % (get_user_by_id(get_max_uid(40000))) + print "Group with max GID is %s" % (get_group_by_id(get_max_gid(40000))) diff --git a/gnuviechadmin/util/passwordutils.py b/gnuviechadmin/util/passwordutils.py index 111e2c6..64211df 100644 --- a/gnuviechadmin/util/passwordutils.py +++ b/gnuviechadmin/util/passwordutils.py @@ -21,16 +21,17 @@ import crypt, crack, random +_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)) + 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.sample(_pwchars, random.randint(minlength, maxlength))]) def checkpassword(password): @@ -45,7 +46,7 @@ def checkpassword(password): 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)]) + random.sample(_pwchars, 8)]) return crypt.crypt(password, '$1$' + salt) def get_pw_tuple(password = None):