- restructured
- implementation of client and sysuser cli - backend for client, sysuser, domain and record - unified cli binary gva git-svn-id: file:///home/www/usr01/svn/gnuviechadmin/gnuviech.info/gnuviechadmin/trunk@226 a67ec6bc-e5d5-0310-a910-815c51eb3124
This commit is contained in:
parent
ee36146629
commit
926acaddfa
19 changed files with 1010 additions and 345 deletions
115
bin/createclient
115
bin/createclient
|
@ -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=<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()
|
|
16
bin/gva
16
bin/gva
|
@ -24,14 +24,16 @@ import gnuviechadmin.cli.client
|
||||||
import gnuviechadmin.cli.sysuser
|
import gnuviechadmin.cli.sysuser
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
commands = [gnuviechadmin.cli.client.ClientCli,
|
||||||
|
gnuviechadmin.cli.sysuser.SysuserCli]
|
||||||
|
|
||||||
def usage():
|
def usage():
|
||||||
print """%s <command> [commandargs]
|
print """%s <command> [commandargs]
|
||||||
|
|
||||||
where command is one of
|
where command is one of
|
||||||
|
|
||||||
client - for creating clients
|
|
||||||
sysuser - for creating system users
|
|
||||||
""" % sys.argv[0]
|
""" % sys.argv[0]
|
||||||
|
for command in commands:
|
||||||
|
print "%10s - %s" % (command.name, command.description)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if (sys.argv.__len__() < 2):
|
if (sys.argv.__len__() < 2):
|
||||||
|
@ -39,10 +41,10 @@ def main():
|
||||||
sys.exit()
|
sys.exit()
|
||||||
command = sys.argv[1]
|
command = sys.argv[1]
|
||||||
commargs = sys.argv[2:]
|
commargs = sys.argv[2:]
|
||||||
if command == "client":
|
if command in [cmd.name for cmd in commands]:
|
||||||
gnuviechadmin.cli.client.ClientCli(commargs)
|
for cmd in commands:
|
||||||
elif command == "sysuser":
|
if cmd.name == command:
|
||||||
gnuviechadmin.cli.sysuser.SysuserCli(commargs)
|
cmd(commargs)
|
||||||
else:
|
else:
|
||||||
usage()
|
usage()
|
||||||
|
|
||||||
|
|
83
gnuviechadmin/backend/BackendEntity.py
Normal file
83
gnuviechadmin/backend/BackendEntity.py
Normal file
|
@ -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)
|
69
gnuviechadmin/backend/BackendEntityHandler.py
Normal file
69
gnuviechadmin/backend/BackendEntityHandler.py
Normal file
|
@ -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)
|
13
bin/listclients → gnuviechadmin/backend/__init__.py
Executable file → Normal file
13
bin/listclients → gnuviechadmin/backend/__init__.py
Executable file → Normal file
|
@ -1,4 +1,3 @@
|
||||||
#!/usr/bin/python
|
|
||||||
# -*- coding: UTF-8 -*-
|
# -*- coding: UTF-8 -*-
|
||||||
#
|
#
|
||||||
# Copyright (C) 2007 by Jan Dittberner.
|
# Copyright (C) 2007 by Jan Dittberner.
|
||||||
|
@ -17,12 +16,10 @@
|
||||||
# along with this program; if not, write to the Free Software
|
# along with this program; if not, write to the Free Software
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
|
||||||
# USA.
|
# USA.
|
||||||
|
#
|
||||||
|
# Version: $Id$
|
||||||
|
|
||||||
from gnuviechadmin import client
|
"""This is the gnuviechadmin.backend package.
|
||||||
|
|
||||||
def main():
|
The package provides the backend entities and supporting code for
|
||||||
for row in client.fetchall():
|
gnuviechadmin."""
|
||||||
print row
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
59
gnuviechadmin/backend/client.py
Normal file
59
gnuviechadmin/backend/client.py
Normal file
|
@ -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)
|
50
gnuviechadmin/backend/domain.py
Normal file
50
gnuviechadmin/backend/domain.py
Normal file
|
@ -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)
|
52
gnuviechadmin/backend/record.py
Normal file
52
gnuviechadmin/backend/record.py
Normal file
|
@ -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)
|
153
gnuviechadmin/backend/sysuser.py
Normal file
153
gnuviechadmin/backend/sysuser.py
Normal file
|
@ -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)
|
|
@ -52,9 +52,42 @@ sysuser_table = Table(
|
||||||
Column('home', String(128)),
|
Column('home', String(128)),
|
||||||
Column('shell', Boolean, nullable=False, default=False),
|
Column('shell', Boolean, nullable=False, default=False),
|
||||||
Column('clearpass', String(64)),
|
Column('clearpass', String(64)),
|
||||||
Column('md5pass', String(32)),
|
Column('md5pass', String(34)),
|
||||||
Column('clientid', Integer, ForeignKey("client.clientid"), nullable=False),
|
Column('clientid', Integer, ForeignKey("client.clientid"), nullable=False),
|
||||||
Column('sysuid', Integer, nullable=False, unique=True),
|
Column('sysuid', Integer, nullable=False, unique=True),
|
||||||
Column('lastchange', DateTime, default=func.now())
|
Column('lastchange', DateTime, default=func.now())
|
||||||
)
|
)
|
||||||
sysuser_table.create(checkfirst=True)
|
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)
|
|
@ -20,62 +20,202 @@
|
||||||
# Version: $Id$
|
# Version: $Id$
|
||||||
|
|
||||||
import getopt, sys
|
import getopt, sys
|
||||||
|
from gnuviechadmin.exceptions import GnuviechadminError
|
||||||
|
|
||||||
class CliCommand:
|
class CliCommand:
|
||||||
"""Base class for command line interface."""
|
"""Base class for command line interface. A specific
|
||||||
def usage(self):
|
implementation class must define the fields name, description and
|
||||||
"""This method should print usage information for the command."""
|
_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
|
raise NotImplementedError
|
||||||
|
|
||||||
def shortopts(self):
|
def _parseopts(self, subcommand, args):
|
||||||
"""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):
|
|
||||||
"""This method parses the options given on the command line."""
|
"""This method parses the options given on the command line."""
|
||||||
longopts = ["help", "verbose"]
|
longopts = ["help", "verbose"]
|
||||||
longopts.extend(self.longopts())
|
longopts.extend(self._longopts(subcommand))
|
||||||
try:
|
try:
|
||||||
opts, args = getopt.gnu_getopt(
|
opts, args = getopt.gnu_getopt(
|
||||||
args,
|
args,
|
||||||
"hv" + self.shortopts(),
|
"hv" + self._shortopts(subcommand),
|
||||||
longopts)
|
longopts)
|
||||||
except getopt.GetoptError:
|
except getopt.GetoptError:
|
||||||
self.usage()
|
self._usage()
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
self.verbose = False
|
self._verbose = False
|
||||||
for o, a in opts:
|
for o, a in opts:
|
||||||
if o in ("-v", "--verbose"):
|
if o in ("-v", "--verbose"):
|
||||||
self.verbose = True
|
self._verbose = True
|
||||||
if o in ("-h", "--help"):
|
if o in ("-h", "--help"):
|
||||||
self.usage()
|
self._usage()
|
||||||
sys.exit()
|
sys.exit()
|
||||||
self.handleoption(o, a)
|
self._handleoption(subcommand, o, a)
|
||||||
|
|
||||||
def __init__(self, args):
|
def __init__(self, args):
|
||||||
"""This initializes the command with the given command line
|
"""This initializes the command with the given command line
|
||||||
arguments and executes it."""
|
arguments and executes it."""
|
||||||
self.__parseopts(args)
|
self._data = {}
|
||||||
if (self.checkrequired()):
|
if len(args) > 0:
|
||||||
self.execute()
|
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:
|
else:
|
||||||
self.usage()
|
self._usage()
|
||||||
|
|
|
@ -20,91 +20,63 @@
|
||||||
# Version: $Id$
|
# Version: $Id$
|
||||||
|
|
||||||
import CliCommand, sys
|
import CliCommand, sys
|
||||||
from gnuviechadmin import client
|
|
||||||
|
|
||||||
class ClientCli(CliCommand.CliCommand):
|
class ClientCli(CliCommand.CliCommand):
|
||||||
"""Command line interface command for client creation."""
|
"""Command line interface command for client managament."""
|
||||||
def shortopts(self):
|
|
||||||
return "f:l:a:z:c:e:p:o:t:m:x:"
|
|
||||||
|
|
||||||
def longopts(self):
|
name = "client"
|
||||||
return ["firstname=", "lastname=", "address=", "zip=",
|
description = "manage clients"
|
||||||
"city=", "email=", "phone=", "address2=", "organisation=",
|
_optionmap = {
|
||||||
"country=", "title=", "mobile=", "fax="]
|
'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 usage(self):
|
def _execute(self, subcommand):
|
||||||
print """Usage: %s client [-h|--help] [-v|--verbose]
|
from gnuviechadmin.backend import client
|
||||||
[-t <title>|--title=<title>]
|
from gnuviechadmin import exceptions
|
||||||
-f <firstname>|--firstname=<firstname> -l <lastname>|--lastname=<lastname>
|
if subcommand == "create":
|
||||||
-a <address1>|--address=<address1> [--address2=<address2>]
|
try:
|
||||||
-z <zip>|--zip=<zip> -c <city>|--city=<city> [--country=<isocode>]
|
myclient = client.ClientHandler(self._verbose).create(
|
||||||
[-o <organisation>|--organisation=<organisation>]
|
**self._data)
|
||||||
-e <email>|--email=<email> -p <phone>|--phone=<phone>
|
if self._verbose:
|
||||||
[-m <mobile>|--mobile=<mobile>] [-x <fax>|--fax=<fax>]
|
print myclient
|
||||||
|
except exceptions.CreationFailedError, cfe:
|
||||||
General options:
|
self._usage()
|
||||||
-h, --help show this usage message and exit
|
print cfe
|
||||||
-v, --verbose verbose operation
|
sys.exit(2)
|
||||||
|
elif subcommand == "list":
|
||||||
Mandatory client data options:
|
clients = client.ClientHandler(self._verbose).fetchall()
|
||||||
-f, --firstname firstname
|
for client in clients:
|
||||||
-l, --lastname lastname
|
print client
|
||||||
-a, --address street address
|
elif subcommand == "delete":
|
||||||
-z, --zip zip or postal code
|
client.ClientHandler(self._verbose).delete(self._data["clientid"])
|
||||||
-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
|
|
||||||
|
|
||||||
def __init__(self, argv):
|
def __init__(self, argv):
|
||||||
self.data = {}
|
|
||||||
CliCommand.CliCommand.__init__(self, argv)
|
CliCommand.CliCommand.__init__(self, argv)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
|
|
@ -32,3 +32,19 @@
|
||||||
# very usable for a real installation.
|
# very usable for a real installation.
|
||||||
#
|
#
|
||||||
uri = sqlite:///:memory:
|
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
|
||||||
|
|
|
@ -20,18 +20,24 @@
|
||||||
# Version: $Id$
|
# Version: $Id$
|
||||||
|
|
||||||
"""This file defines the gnuviechadmin specific exception types."""
|
"""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
|
"""This exception should be raised when a required field of a data
|
||||||
class is missing."""
|
class is missing."""
|
||||||
def __init__(self, missingfields):
|
def __init__(self, missingfields):
|
||||||
self.missing = missingfields
|
self.missing = missingfields
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "the fields %s are missing." % (repr(self.missing))
|
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
|
"""This exception should be raised if a business object could not
|
||||||
be created."""
|
be created."""
|
||||||
def __init__(self, classname, cause = None):
|
def __init__(self, classname, cause = None):
|
||||||
self.classname = classname
|
self.classname = classname
|
||||||
self.cause = cause
|
self.cause = cause
|
||||||
|
@ -41,3 +47,16 @@ be created."""
|
||||||
if self.cause:
|
if self.cause:
|
||||||
msg += " The reason is %s." % (str(self.cause))
|
msg += " The reason is %s." % (str(self.cause))
|
||||||
return msg
|
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
|
||||||
|
|
|
@ -19,16 +19,6 @@
|
||||||
#
|
#
|
||||||
# Version: $Id$
|
# Version: $Id$
|
||||||
|
|
||||||
from sqlalchemy import *
|
"""This is the gnuviechadmin.util package.
|
||||||
from gnuviechadmin.tables import *
|
|
||||||
from gnuviechadmin.exceptions import *
|
|
||||||
|
|
||||||
class Sysuser(object):
|
The package provides utility modules for various functions."""
|
||||||
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)
|
|
94
gnuviechadmin/util/getenttools.py
Normal file
94
gnuviechadmin/util/getenttools.py
Normal file
|
@ -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)))
|
57
gnuviechadmin/util/passwordutils.py
Normal file
57
gnuviechadmin/util/passwordutils.py
Normal file
|
@ -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))
|
Loading…
Reference in a new issue