diff --git a/CHANGELOG b/CHANGELOG index 7a58a7bd49..dc87321d25 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Refactor configuration file and command line * Use the context of the relation field for instanciation * Use a configuration field for logging * Add translated descriptor for Selection field diff --git a/MANIFEST.in b/MANIFEST.in index 15940c4500..b5f249fa39 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,7 +8,6 @@ include doc/* recursive-include doc *.rst recursive-include doc *.po recursive-include doc *.pot -include etc/* include trytond/backend/*/init.sql include trytond/ir/tryton.cfg include trytond/ir/*.xml diff --git a/bin/trytond b/bin/trytond index 1e43cacb0e..5519f342cb 100755 --- a/bin/trytond +++ b/bin/trytond @@ -21,72 +21,39 @@ def parse_commandline(): parser.add_argument('--version', action='version', version='%(prog)s ' + VERSION) - parser.add_argument("-c", "--config", dest="config", + parser.add_argument("-c", "--config", dest="configfile", help="specify config file") - parser.add_argument('--debug', dest='debug_mode', action='store_true', - help='enable debug mode (start post-mortem debugger if exceptions' - ' occur)') + parser.add_argument('--debug', dest='debug', action='store_true', + help='enable debug mode') parser.add_argument("-v", "--verbose", action="store_true", dest="verbose", help="enable verbose mode") - parser.add_argument("-d", "--database", dest="db_name", - help="specify the database name") - parser.add_argument("-i", "--init", dest="init", + parser.add_argument("-d", "--database", dest="database_names", nargs='+', + default=[], help="specify the database name") + parser.add_argument("-i", "--init", dest="init", nargs='+', default=[], help="init a module (use \"all\" for all modules)") - parser.add_argument("-u", "--update", dest="update", + parser.add_argument("-u", "--update", dest="update", nargs='+', default=[], help="update a module (use \"all\" for all modules)") parser.add_argument("--pidfile", dest="pidfile", help="file where the server pid will be stored") parser.add_argument("--logconf", dest="logconf", help="logging configuration file (ConfigParser format)") - parser.add_argument("--disable-cron", dest="cron", - action="store_false", help="disable cron") + parser.add_argument("--cron", dest="cron", action="store_true", + help="enable cron") parser.epilog = ('The first time a database is initialized with "-i" admin' ' password is read from file defined by TRYTONPASSFILE ' - 'environment variable or interactively ask user. ' + 'environment variable or interactively ask user.\n' 'The config file can be specified in the TRYTOND_CONFIG ' + 'environment variable.\n' + 'The database URI can be specified in the TRYTOND_DATABASE_URI ' 'environment variable.') - opt = parser.parse_args() - - if opt.config: - options['configfile'] = opt.config - else: - # No config file speficified, it will be guessed - options['configfile'] = None - - for arg in ( - 'verbose', - 'debug_mode', - 'pidfile', - 'logconf', - 'cron', - ): - if getattr(opt, arg) is not None: - options[arg] = getattr(opt, arg) - - db_name = [] - if opt.db_name: - for i in opt.db_name.split(','): - db_name.append(i) - options['db_name'] = db_name - - init = {} - if opt.init: - for i in opt.init.split(','): - if i != 'test': - init[i] = 1 - options['init'] = init - - update = {} - if opt.update: - for i in opt.update.split(','): - if i != 'test': - update[i] = 1 - options['update'] = update + options = parser.parse_args() + if not options.database_names and bool(options.init or options.update): + parser.error('Missing database option') return options diff --git a/doc/index.rst b/doc/index.rst index aa1c79bc89..5bb8625cc5 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -25,6 +25,7 @@ First steps * **Installation:** :ref:`Installation ` + :ref:`Configuration ` The model layer =============== diff --git a/doc/topics/configuration.rst b/doc/topics/configuration.rst new file mode 100644 index 0000000000..a187c0ba7c --- /dev/null +++ b/doc/topics/configuration.rst @@ -0,0 +1,193 @@ +.. _topics-configuration: + +============================= +Configuration file for Tryton +============================= + +The configuration file control some aspects of the behavior of Tryton. +The file uses a simple ini-file format. It consists of sections, led by a +`[section]` header and followed by `name = value` entries: + + [database] + uri = postgresql://user:password@localhost/ + path = /var/lib/trytond + +For more information see ConfigParser_. + +.. _ConfigParser: http://docs.python.org/2/library/configparser.html + +Sections +======== + +This section describes the different main sections that may appear in a Tryton +configuration file, the purpose of each section, its possible keys, and their +possible values. +Some modules could request the usage of other sections for which the guideline +asks them to be named like their module. + +jsonrpc +------- + +Defines the behavior of the JSON-RPC_ network interface. + +listen +~~~~~~ + +Defines a comma separated list of couple of host (or IP address) and port numer +separeted by a colon to listen on. +The default value is `localhost:8000`. + +hostname +~~~~~~~~ + +Defines the hostname for this network interface. + +data +~~~~ + +Defines the root path to retrieve data for `GET` request. + +xmlrpc +------ + +Defines the behavior of the XML-RPC_ network interface. + +listen +~~~~~~ + +Same as for `jsonrpc` except it have no default value. + +webdav +------ + +Define the behavior of the WebDAV_ network interface. + +listen +~~~~~~ + +Same as for `jsonrpc` except it have no default value. + +database +-------- + +Defines how database is managed. + +uri +~~~ + +Contains the URI to connect to the SQL database. The URI follows the RFC-3986_. +The typical form is: + + database://username:password@host:port/ + +The default available databases are: + +PostgreSQL +********** + +`pyscopg2` supports two type of connections: + + - TCP/IP connection: `postgresql://user:password@localhost:5432/` + - Unix domain connection: `postgresql://username:password@/` + +SQLite +****** + +The only possible URI is: `sqlite://` + +MySQL +***** + +Same as for PostgreSQL. + +path +~~~~ + +The directory where Tryton should store files and so the user running `trytond` +must have write access on this directory. +The default value is `/var/lib/trytond/`. + +list +~~~~ + +A boolean value (default: `True`) to list available databases. + +retry +~~~~~ + +The number of retries when a database operation error occurs during a request. + +language +~~~~~~~~ + +The main language (default: `en_US`) of the database that will be stored in the +main table for translatable fields. + +ssl +--- + +Activates the SSL_ on all network protocol. + +privatekey +~~~~~~~~~~ + +The path to the private key. + +certificate +~~~~~~~~~~~ + +The path to the certificate. + +email +----- + +uri +~~~ + +The SMTP-URL_ to connect to the SMTP server which is extended to support SSL_ +and STARTTLS_. +The available protocols are: + + - `smtp`: simple SMTP + - `smtp+tls`: SMTP with STARTTLS + - `smtps`: SMTP with SSL + +The default value is: `smtp://localhost:25` + +from +~~~~ + +Defines the default `From` address when Tryton send emails. + +session +------- + +timeout +~~~~~~~ + +The time in second before a session expires. + +super_pwd +~~~~~~~~~ + +Theserver password uses to authenticate database management from the client. +It is encrypted using using the Unix `crypt(3)` routine. +Such password can be generated using this command line:: + + python -c 'import getpass,crypt,random,string; print crypt.crypt(getpass.getpass(), "".join(random.sample(string.ascii_letters + string.digits, 8)))' + +report +------ + +unoconv +~~~~~~~ + +The parameters for `unoconv`. + +.. _JSON-RPC: http://en.wikipedia.org/wiki/JSON-RPC +.. _XML-RPC: http://en.wikipedia.org/wiki/XML-RPC +.. _WebDAV: http://en.wikipedia.org/wiki/WebDAV +.. _RFC-3986: http://tools.ietf.org/html/rfc3986 +.. _SMTP-URL: http://tools.ietf.org/html/draft-earhart-url-smtp-00 +.. _SSL: http://en.wikipedia.org/wiki/Secure_Sockets_Layer +.. _STARTTLS: http://en.wikipedia.org/wiki/STARTTLS diff --git a/doc/topics/index.rst b/doc/topics/index.rst index a74763bdb9..ba34d474be 100644 --- a/doc/topics/index.rst +++ b/doc/topics/index.rst @@ -10,6 +10,7 @@ Introduction to all the key parts of trytond: :maxdepth: 1 install + configuration logs models/index models/fields_default_value diff --git a/etc/trytond.conf b/etc/trytond.conf deleted file mode 100644 index b2e70cdf51..0000000000 --- a/etc/trytond.conf +++ /dev/null @@ -1,88 +0,0 @@ -#This file is part of Tryton. The COPYRIGHT file at the top level of -#this repository contains the full copyright notices and license terms. -[options] - -# Activate the json-rpc protocol -jsonrpc = localhost:8000 -#ssl_jsonrpc = False - -# This is the hostname used when generating tryton URI -#hostname_jsonrpc = - -# Configure the path of json-rpc data -#jsondata_path = /var/www/localhost/tryton - -# Activate the xml-rpc protocol -#xmlrpc = *:8069 -#ssl_xmlrpc = False - -# Activate the webdav protocol -#webdav = *:8080 -#ssl_webdav = False - -# This is the hostname used when generating WebDAV URI -#hostname_webdav = - -# Configure the database type -# allowed values are postgresql, sqlite, mysql -#db_type = postgresql - -# Configure the database connection -## Note: Only databases owned by db_user will be displayed in the connection dialog -## of the Tryton client. db_user must have create permission for new databases -## to be able to use automatic database creation with the Tryton client. -#db_host = False -#db_port = False -#db_user = False -#db_password = False -#db_minconn = 1 -#db_maxconn = 64 - -# Configure the postgresql path for the executable -#pg_path = None - -# Configure the Tryton server password -#admin_passwd = admin - -# Configure the path of the files for the pid and the logs -#pidfile = False -#logconf = False - -#privatekey = server.pem -#certificate = server.pem - -# Configure the SMTP connection -#smtp_server = localhost -#smtp_port = 25 -#smtp_ssl = False -#smtp_tls = False -#smtp_password = False -#smtp_user = False -#smtp_default_from_email = False - -# Configure the path to store attachments and sqlite database -#data_path = /var/lib/trytond - -# Allow to run more than one instance of trytond -#multi_server = False - -# Configure the session timeout (inactivity of the client in sec) -#session_timeout = 600 - -# Enable auto-reload of modules if changed -#auto_reload = True - -# Prevent database listing -#prevent_dblist = False - -# Enable cron -# cron = True - -# unoconv connection -#unoconv = pipe,name=trytond;urp;StarOffice.ComponentContext - -# Number of retries on database operational error -# retry = 5 - -# Default language code -# language = en_US diff --git a/trytond/backend/__init__.py b/trytond/backend/__init__.py index 56202911f2..6b9abc47a9 100644 --- a/trytond/backend/__init__.py +++ b/trytond/backend/__init__.py @@ -1,15 +1,20 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. import sys +import urlparse -from trytond.config import CONFIG +from trytond.config import config -__all__ = ['get'] +__all__ = ['name', 'get'] -def get(name): - db_type = CONFIG['db_type'] +def name(): + return urlparse.urlparse(config.get('database', 'uri', '')).scheme + + +def get(prop): + db_type = name() modname = 'trytond.backend.%s' % db_type __import__(modname) module = sys.modules[modname] - return getattr(module, name) + return getattr(module, prop) diff --git a/trytond/backend/mysql/database.py b/trytond/backend/mysql/database.py index f61136f56c..7f5b8e7148 100644 --- a/trytond/backend/mysql/database.py +++ b/trytond/backend/mysql/database.py @@ -1,7 +1,7 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. from trytond.backend.database import DatabaseInterface, CursorInterface -from trytond.config import CONFIG +from trytond.config import config, parse_uri import MySQLdb import MySQLdb.cursors import MySQLdb.converters @@ -91,14 +91,16 @@ class Database(DatabaseInterface): 'charset': 'utf8', 'conv': conv, } - if CONFIG['db_host']: - args['host'] = CONFIG['db_host'] - if CONFIG['db_port']: - args['port'] = int(CONFIG['db_port']) - if CONFIG['db_user']: - args['user'] = CONFIG['db_user'] - if CONFIG['db_password']: - args['passwd'] = CONFIG['db_password'] + uri = parse_uri(config.get('database', 'uri')) + assert uri.scheme == 'mysql' + if uri.hostname: + args['host'] = uri.hostname + if uri.port: + args['port'] = uri.port + if uri.username: + args['user'] = uri.username + if uri.password: + args['passwd'] = uri.password conn = MySQLdb.connect(**args) cursor = Cursor(conn, self.database_name) cursor.execute('SET time_zone = `UTC`') @@ -120,20 +122,21 @@ class Database(DatabaseInterface): @staticmethod def dump(database_name): - from trytond.tools import exec_pg_command_pipe + from trytond.tools import exec_command_pipe cmd = ['mysqldump', '--no-create-db'] - if CONFIG['db_user']: - cmd.append('--user=' + CONFIG['db_user']) - if CONFIG['db_host']: - cmd.append('--host=' + CONFIG['db_host']) - if CONFIG['db_port']: - cmd.append('--port=' + CONFIG['db_port']) - if CONFIG['db_password']: - cmd.append('--password=' + CONFIG['db_password']) + uri = parse_uri(config.get('database', 'uri')) + if uri.username: + cmd.append('--user=' + uri.username) + if uri.hostname: + cmd.append('--host=' + uri.hostname) + if uri.port: + cmd.append('--port=' + uri.port) + if uri.password: + cmd.append('--password=' + uri.password) cmd.append(database_name) - pipe = exec_pg_command_pipe(*tuple(cmd)) + pipe = exec_command_pipe(*tuple(cmd)) pipe.stdin.close() data = pipe.stdout.read() res = pipe.wait() @@ -143,7 +146,7 @@ class Database(DatabaseInterface): @staticmethod def restore(database_name, data): - from trytond.tools import exec_pg_command_pipe + from trytond.tools import exec_command_pipe database = Database().connect() cursor = database.cursor(autocommit=True) @@ -152,14 +155,15 @@ class Database(DatabaseInterface): cursor.close() cmd = ['mysql'] - if CONFIG['db_user']: - cmd.append('--user=' + CONFIG['db_user']) - if CONFIG['db_host']: - cmd.append('--host=' + CONFIG['db_host']) - if CONFIG['db_port']: - cmd.append('--port=' + CONFIG['db_port']) - if CONFIG['db_password']: - cmd.append('--password=' + CONFIG['db_password']) + uri = parse_uri(config.get('database', 'uri')) + if uri.username: + cmd.append('--user=' + uri.username) + if uri.hostname: + cmd.append('--host=' + uri.hostname) + if uri.port: + cmd.append('--port=' + uri.port) + if uri.password: + cmd.append('--password=' + uri.password) cmd.append(database_name) fd, file_name = tempfile.mkstemp() @@ -171,7 +175,7 @@ class Database(DatabaseInterface): args2 = tuple(cmd) - pipe = exec_pg_command_pipe(*args2) + pipe = exec_command_pipe(*args2) pipe.stdin.close() res = pipe.wait() os.remove(file_name) @@ -192,7 +196,7 @@ class Database(DatabaseInterface): @staticmethod def list(cursor): now = time.time() - timeout = int(CONFIG['session_timeout']) + timeout = config.getint('session', 'timeout') res = Database._list_cache if res and abs(Database._list_cache_timestamp - now) < timeout: return res diff --git a/trytond/backend/postgresql/database.py b/trytond/backend/postgresql/database.py index ca444cde62..83235572c8 100644 --- a/trytond/backend/postgresql/database.py +++ b/trytond/backend/postgresql/database.py @@ -1,7 +1,7 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. from trytond.backend.database import DatabaseInterface, CursorInterface -from trytond.config import CONFIG +from trytond.config import config, parse_uri from psycopg2.pool import ThreadedConnectionPool from psycopg2.extensions import cursor as PsycopgCursor from psycopg2.extensions import ISOLATION_LEVEL_REPEATABLE_READ @@ -54,14 +54,15 @@ class Database(DatabaseInterface): return self logger = logging.getLogger('database') logger.info('connect to "%s"' % self.database_name) - host = CONFIG['db_host'] and "host=%s" % CONFIG['db_host'] or '' - port = CONFIG['db_port'] and "port=%s" % CONFIG['db_port'] or '' + uri = parse_uri(config.get('database', 'uri')) + assert uri.scheme == 'postgresql' + host = uri.hostname and "host=%s" % uri.hostname or '' + port = uri.port and "port=%s" % uri.port or '' name = "dbname=%s" % self.database_name - user = CONFIG['db_user'] and "user=%s" % CONFIG['db_user'] or '' - password = (CONFIG['db_password'] - and "password=%s" % CONFIG['db_password'] or '') - minconn = int(CONFIG['db_minconn']) or 1 - maxconn = int(CONFIG['db_maxconn']) or 64 + user = uri.username and "user=%s" % uri.username or '' + password = uri.password and "password=%s" % uri.password or '' + minconn = config.getint('database', 'minconn', 1) + maxconn = config.getint('database', 'maxconn', 64) dsn = '%s %s %s %s %s' % (host, port, name, user, password) self._connpool = ThreadedConnectionPool(minconn, maxconn, dsn) return self @@ -106,18 +107,25 @@ class Database(DatabaseInterface): @staticmethod def dump(database_name): - from trytond.tools import exec_pg_command_pipe + from trytond.tools import exec_command_pipe cmd = ['pg_dump', '--format=c', '--no-owner'] - if CONFIG['db_user']: - cmd.append('--username=' + CONFIG['db_user']) - if CONFIG['db_host']: - cmd.append('--host=' + CONFIG['db_host']) - if CONFIG['db_port']: - cmd.append('--port=' + CONFIG['db_port']) + env = {} + uri = parse_uri(config.get('database', 'uri')) + if uri.username: + cmd.append('--username=' + uri.username) + if uri.hostname: + cmd.append('--host=' + uri.hostname) + if uri.port: + cmd.append('--port=' + uri.port) + if uri.password: + # if db_password is set in configuration we should pass + # an environment variable PGPASSWORD to our subprocess + # see libpg documentation + env['PGPASSWORD'] = uri.password cmd.append(database_name) - pipe = exec_pg_command_pipe(*tuple(cmd)) + pipe = exec_command_pipe(*tuple(cmd), env=env) pipe.stdin.close() data = pipe.stdout.read() res = pipe.wait() @@ -127,7 +135,7 @@ class Database(DatabaseInterface): @staticmethod def restore(database_name, data): - from trytond.tools import exec_pg_command_pipe + from trytond.tools import exec_command_pipe database = Database().connect() cursor = database.cursor(autocommit=True) @@ -136,12 +144,16 @@ class Database(DatabaseInterface): cursor.close() cmd = ['pg_restore', '--no-owner'] - if CONFIG['db_user']: - cmd.append('--username=' + CONFIG['db_user']) - if CONFIG['db_host']: - cmd.append('--host=' + CONFIG['db_host']) - if CONFIG['db_port']: - cmd.append('--port=' + CONFIG['db_port']) + env = {} + uri = parse_uri(config.get('database', 'uri')) + if uri.username: + cmd.append('--username=' + uri.username) + if uri.hostname: + cmd.append('--host=' + uri.hostname) + if uri.port: + cmd.append('--port=' + uri.port) + if uri.password: + env['PGPASSWORD'] = uri.password cmd.append('--dbname=' + database_name) args2 = tuple(cmd) @@ -153,7 +165,7 @@ class Database(DatabaseInterface): args2.append(' ' + tmpfile) args2 = tuple(args2) - pipe = exec_pg_command_pipe(*args2) + pipe = exec_command_pipe(*args2, env=env) if not os.name == "nt": pipe.stdin.write(data) pipe.stdin.close() @@ -175,23 +187,14 @@ class Database(DatabaseInterface): @staticmethod def list(cursor): now = time.time() - timeout = int(CONFIG['session_timeout']) + timeout = config.getint('session', 'timeout') res = Database._list_cache if res and abs(Database._list_cache_timestamp - now) < timeout: return res - db_user = CONFIG['db_user'] + uri = parse_uri(config.get('database', 'uri')) + db_user = uri.username if not db_user and os.name == 'posix': db_user = pwd.getpwuid(os.getuid())[0] - if not db_user: - cursor.execute("SELECT usename " - "FROM pg_user " - "WHERE usesysid = (" - "SELECT datdba " - "FROM pg_database " - "WHERE datname = %s)", - (CONFIG["db_name"],)) - res = cursor.fetchone() - db_user = res and res[0] if db_user: cursor.execute("SELECT datname " "FROM pg_database " diff --git a/trytond/backend/sqlite/database.py b/trytond/backend/sqlite/database.py index d374522244..7ee1e2b7dd 100644 --- a/trytond/backend/sqlite/database.py +++ b/trytond/backend/sqlite/database.py @@ -1,7 +1,7 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. from trytond.backend.database import DatabaseInterface, CursorInterface -from trytond.config import CONFIG +from trytond.config import config import os from decimal import Decimal import datetime @@ -174,7 +174,7 @@ class Database(DatabaseInterface): path = ':memory:' else: db_filename = self.database_name + '.sqlite' - path = os.path.join(CONFIG['data_path'], db_filename) + path = os.path.join(config.get('database', 'path'), db_filename) if not os.path.isfile(path): raise IOError('Database "%s" doesn\'t exist!' % db_filename) if self._conn is not None: @@ -216,7 +216,7 @@ class Database(DatabaseInterface): else: if os.sep in database_name: return - path = os.path.join(CONFIG['data_path'], + path = os.path.join(config.get('database', 'path'), database_name + '.sqlite') with sqlite.connect(path) as conn: cursor = conn.cursor() @@ -229,7 +229,7 @@ class Database(DatabaseInterface): return if os.sep in database_name: return - os.remove(os.path.join(CONFIG['data_path'], + os.remove(os.path.join(config.get('database', 'path'), database_name + '.sqlite')) @staticmethod @@ -238,7 +238,7 @@ class Database(DatabaseInterface): raise Exception('Unable to dump memory database!') if os.sep in database_name: raise Exception('Wrong database name!') - path = os.path.join(CONFIG['data_path'], + path = os.path.join(config.get('database', 'path'), database_name + '.sqlite') with open(path, 'rb') as file_p: data = file_p.read() @@ -250,7 +250,7 @@ class Database(DatabaseInterface): raise Exception('Unable to restore memory database!') if os.sep in database_name: raise Exception('Wrong database name!') - path = os.path.join(CONFIG['data_path'], + path = os.path.join(config.get('database', 'path'), database_name + '.sqlite') if os.path.isfile(path): raise Exception('Database already exists!') @@ -262,7 +262,7 @@ class Database(DatabaseInterface): res = [] listdir = [':memory:'] try: - listdir += os.listdir(CONFIG['data_path']) + listdir += os.listdir(config.get('database', 'path')) except OSError: pass for db_file in listdir: diff --git a/trytond/cache.py b/trytond/cache.py index c6d7adb991..6ccd0392fd 100644 --- a/trytond/cache.py +++ b/trytond/cache.py @@ -1,12 +1,12 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. -import datetime from threading import Lock from collections import OrderedDict +from sql import Table +from sql.functions import Now + from trytond.transaction import Transaction -from trytond.config import CONFIG -from trytond import backend __all__ = ['Cache', 'LRUDict'] @@ -74,18 +74,13 @@ class Cache(object): @staticmethod def clean(dbname): - if not CONFIG['multi_server']: - return - database = backend.get('Database')(dbname).connect() - cursor = database.cursor() - try: - cursor.execute('SELECT "timestamp", "name" FROM ir_cache') + with Transaction().new_cursor(): + cursor = Transaction().cursor + table = Table('ir_cache') + cursor.execute(*table.select(table.timestamp, table.name)) timestamps = {} for timestamp, name in cursor.fetchall(): timestamps[name] = timestamp - finally: - cursor.commit() - cursor.close() for inst in Cache._cache_instance: if inst._name in timestamps: with inst._lock: @@ -96,36 +91,30 @@ class Cache(object): @staticmethod def reset(dbname, name): - if not CONFIG['multi_server']: - return with Cache._resets_lock: Cache._resets.setdefault(dbname, set()) Cache._resets[dbname].add(name) @staticmethod def resets(dbname): - if not CONFIG['multi_server']: - return - database = backend.get('Database')(dbname).connect() - cursor = database.cursor() - try: + with Transaction().new_cursor(): + cursor = Transaction().cursor + table = Table('ir_cache') with Cache._resets_lock: Cache._resets.setdefault(dbname, set()) for name in Cache._resets[dbname]: - cursor.execute('SELECT name FROM ir_cache WHERE name = %s', - (name,)) + cursor.execute(*table.select(table.name, + where=table.name == name)) if cursor.fetchone(): # It would be better to insert only - cursor.execute('UPDATE ir_cache SET "timestamp" = %s ' - 'WHERE name = %s', (datetime.datetime.now(), name)) + cursor.execute(*table.update([table.timestamp], + [Now()], where=table.name == name)) else: - cursor.execute('INSERT INTO ir_cache ' - '("timestamp", "name") ' - 'VALUES (%s, %s)', (datetime.datetime.now(), name)) + cursor.execute(*table.insert( + [table.timestamp, table.name], + [[Now(), name]])) Cache._resets[dbname].clear() - finally: cursor.commit() - cursor.close() class LRUDict(OrderedDict): diff --git a/trytond/config.py b/trytond/config.py index 5c52e69d1b..e885e5af4d 100644 --- a/trytond/config.py +++ b/trytond/config.py @@ -1,18 +1,11 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. -import sys -try: - import cdecimal - # Use cdecimal globally - if 'decimal' not in sys.modules: - sys.modules['decimal'] = cdecimal -except ImportError: - import decimal - sys.modules['cdecimal'] = decimal import os import ConfigParser -import getpass -import socket +import urlparse + +__all__ = ['config', 'get_hostname', 'get_port', 'split_netloc', + 'parse_listen', 'parse_uri'] def get_hostname(netloc): @@ -24,119 +17,77 @@ def get_hostname(netloc): return netloc -def get_port(netloc, protocol): +def get_port(netloc): netloc = netloc.split(']')[-1] - if ':' in netloc: - return int(netloc.split(':')[1]) - else: - return { - 'jsonrpc': 8000, - 'xmlrpc': 8069, - 'webdav': 8080, - }.get(protocol) + return int(netloc.split(':')[1]) -class ConfigManager(object): - def __init__(self, fname=None): - self.options = { - 'jsonrpc': [('localhost', 8000)], - 'ssl_jsonrpc': False, - 'hostname_jsonrpc': None, - 'xmlrpc': [], - 'ssl_xmlrpc': False, - 'jsondata_path': '/var/www/localhost/tryton', - 'webdav': [], - 'ssl_webdav': False, - 'hostname_webdav': None, - 'db_type': 'postgresql', - 'db_host': False, - 'db_port': False, - 'db_name': False, - 'db_user': False, - 'db_password': False, - 'db_minconn': 1, - 'db_maxconn': 64, - 'pg_path': None, - 'admin_passwd': 'admin', - 'verbose': False, - 'debug_mode': False, - 'pidfile': None, - 'logconf': None, - 'privatekey': '/etc/ssl/trytond/server.key', - 'certificate': '/etc/ssl/trytond/server.pem', - 'smtp_server': 'localhost', - 'smtp_port': 25, - 'smtp_ssl': False, - 'smtp_tls': False, - 'smtp_user': False, - 'smtp_password': False, - 'smtp_default_from_email': '%s@%s' % ( - getpass.getuser(), socket.getfqdn()), - 'data_path': '/var/lib/trytond', - 'multi_server': False, - 'session_timeout': 600, - 'auto_reload': True, - 'prevent_dblist': False, - 'init': {}, - 'update': {}, - 'cron': True, - 'unoconv': 'pipe,name=trytond;urp;StarOffice.ComponentContext', - 'retry': 5, - 'language': 'en_US', - } - self.configfile = None +def split_netloc(netloc): + return get_hostname(netloc).replace('*', ''), get_port(netloc) - def update_cmdline(self, cmdline_options): - self.options.update(cmdline_options) - # Verify that logging configuration is defined in an external file, - # if not the output will go to stdout - if self.options['logconf'] in ('None', 'False'): - self.options['logconf'] = False - # the same for the pidfile - if self.options['pidfile'] in ('None', 'False'): - self.options['pidfile'] = False - if self.options['data_path'] in ('None', 'False'): - self.options['data_path'] = False +def parse_listen(value): + for netloc in value.split(','): + yield split_netloc(netloc) - def update_etc(self, configfile=None): - if configfile is None: - configfile = os.environ.get('TRYTOND_CONFIG') - if not configfile: - prefixdir = os.path.abspath(os.path.normpath(os.path.join( - os.path.dirname(sys.prefix), '..'))) - configfile = os.path.join(prefixdir, 'etc', 'trytond.conf') - if not os.path.isfile(configfile): - configdir = os.path.abspath(os.path.normpath(os.path.join( - os.path.dirname(__file__), '..'))) - configfile = os.path.join(configdir, 'etc', 'trytond.conf') - if not os.path.isfile(configfile): - configfile = None - self.configfile = configfile - if not self.configfile: +def parse_uri(uri): + return urlparse.urlparse(uri) + + +class TrytonConfigParser(ConfigParser.RawConfigParser): + + def __init__(self): + ConfigParser.RawConfigParser.__init__(self) + self.add_section('jsonrpc') + self.set('jsonrpc', 'listen', 'localhost:8000') + self.set('jsonrpc', 'data', '/var/www/localhost/tryton') + self.add_section('xmlrpc') + self.add_section('webdav') + self.add_section('database') + self.set('database', 'uri', + os.environ.get('TRYTOND_DATABASE_URI', 'sqlite://')) + self.set('database', 'path', '/var/lib/trytond') + self.set('database', 'list', 'True') + self.set('database', 'retry', 5) + self.set('database', 'language', 'en_US') + self.add_section('ssl') + self.add_section('email') + self.set('email', 'uri', 'smtp://localhost:25') + self.add_section('session') + self.set('session', 'timeout', 600) + self.add_section('report') + self.set('report', 'unoconv', + 'pipe,name=trytond;urp;StarOffice.ComponentContext') + + def update_etc(self, configfile=os.environ.get('TRYTOND_CONFIG')): + if not configfile: return + self.read(configfile) - parser = ConfigParser.ConfigParser() - with open(self.configfile) as fp: - parser.readfp(fp) - for (name, value) in parser.items('options'): - if value == 'True' or value == 'true': - value = True - if value == 'False' or value == 'false': - value = False - if name in ('xmlrpc', 'jsonrpc', 'webdav') and value: - value = [(get_hostname(netloc).replace('*', ''), - get_port(netloc, name)) for netloc in value.split(',')] - self.options[name] = value + def get(self, section, option, default=None): + try: + return ConfigParser.RawConfigParser.get(self, section, option) + except ConfigParser.NoOptionError: + return default - def get(self, key, default=None): - return self.options.get(key, default) + def getint(self, section, option, default=None): + try: + return ConfigParser.RawConfigParser.getint(self, section, option) + except (ConfigParser.NoOptionError, TypeError): + return default - def __setitem__(self, key, value): - self.options[key] = value + def getfloat(self, section, option, default=None): + try: + return ConfigParser.RawConfigParser.getfloat(self, section, option) + except (ConfigParser.NoOptionError, TypeError): + return default - def __getitem__(self, key): - return self.options[key] + def getboolean(self, section, option, default=None): + try: + return ConfigParser.RawConfigParser.getboolean( + self, section, option) + except ConfigParser.NoOptionError: + return default -CONFIG = ConfigManager() +config = TrytonConfigParser() diff --git a/trytond/ir/attachment.py b/trytond/ir/attachment.py index d261509e65..a268e21d45 100644 --- a/trytond/ir/attachment.py +++ b/trytond/ir/attachment.py @@ -5,7 +5,7 @@ import hashlib from sql.operators import Concat from ..model import ModelView, ModelSQL, fields -from ..config import CONFIG +from ..config import config from .. import backend from ..transaction import Transaction from ..pyson import Eval @@ -117,7 +117,7 @@ class Attachment(ModelSQL, ModelView): filename = self.digest if self.collision: filename = filename + '-' + str(self.collision) - filename = os.path.join(CONFIG['data_path'], db_name, + filename = os.path.join(config.get('database', 'path'), db_name, filename[0:2], filename[2:4], filename) if name == 'data_size' or format_ == 'size': try: @@ -140,7 +140,7 @@ class Attachment(ModelSQL, ModelView): cursor = Transaction().cursor table = cls.__table__() db_name = cursor.dbname - directory = os.path.join(CONFIG['data_path'], db_name) + directory = os.path.join(config.get('database', 'path'), db_name) if not os.path.isdir(directory): os.makedirs(directory, 0770) digest = hashlib.md5(value).hexdigest() diff --git a/trytond/ir/configuration.py b/trytond/ir/configuration.py index abe72db70e..4ee7307aa5 100644 --- a/trytond/ir/configuration.py +++ b/trytond/ir/configuration.py @@ -2,7 +2,7 @@ #this repository contains the full copyright notices and license terms. from ..model import ModelSQL, ModelSingleton, fields from ..cache import Cache -from ..config import CONFIG +from ..config import config __all__ = ['Configuration'] @@ -15,7 +15,7 @@ class Configuration(ModelSingleton, ModelSQL): @staticmethod def default_language(): - return CONFIG['language'] + return config.get('database', 'language') @classmethod def get_language(cls): @@ -25,6 +25,6 @@ class Configuration(ModelSingleton, ModelSQL): config = cls(1) language = config.language if not language: - language = CONFIG['language'] + language = config.get('database', 'language') cls._get_language_cache.set(None, language) return language diff --git a/trytond/ir/cron.py b/trytond/ir/cron.py index f255284645..2886788f9d 100644 --- a/trytond/ir/cron.py +++ b/trytond/ir/cron.py @@ -13,7 +13,7 @@ from ..tools import safe_eval, get_smtp_server from ..transaction import Transaction from ..pool import Pool from .. import backend -from ..config import CONFIG +from ..config import config __all__ = [ 'Cron', @@ -132,7 +132,7 @@ class Cron(ModelSQL, ModelView): (cron.name, cron.__url__, tb_s), raise_exception=False) - from_addr = CONFIG['smtp_default_from_email'] + from_addr = config.get('email', 'from') to_addr = cron.request_user.email msg = MIMEText(body, _charset='utf-8') diff --git a/trytond/ir/module/module.py b/trytond/ir/module/module.py index 605a614b1d..76dca70cbb 100644 --- a/trytond/ir/module/module.py +++ b/trytond/ir/module/module.py @@ -509,13 +509,14 @@ class ModuleInstallUpgrade(Wizard): modules = Module.search([ ('state', 'in', ['to upgrade', 'to remove', 'to install']), ]) + update = [m.name for m in modules] langs = Lang.search([ ('translatable', '=', True), ]) lang = [x.code for x in langs] transaction.cursor.commit() - if modules: - pool.init(update=True, lang=lang) + if update: + pool.init(update=update, lang=lang) return 'done' diff --git a/trytond/ir/sequence.py b/trytond/ir/sequence.py index d1b3c73e63..9435b8bc4b 100644 --- a/trytond/ir/sequence.py +++ b/trytond/ir/sequence.py @@ -10,14 +10,13 @@ from ..tools import datetime_strftime from ..pyson import Eval, And from ..transaction import Transaction from ..pool import Pool -from ..config import CONFIG from .. import backend __all__ = [ 'SequenceType', 'Sequence', 'SequenceStrict', ] -sql_sequence = CONFIG.options['db_type'] == 'postgresql' +sql_sequence = backend.name() == 'postgresql' class SequenceType(ModelSQL, ModelView): diff --git a/trytond/ir/session.py b/trytond/ir/session.py index e6512aea29..d9beae9fc4 100644 --- a/trytond/ir/session.py +++ b/trytond/ir/session.py @@ -8,7 +8,7 @@ import uuid import datetime from trytond.model import ModelSQL, fields -from trytond.config import CONFIG +from trytond.config import config from .. import backend from ..transaction import Transaction @@ -45,7 +45,8 @@ class Session(ModelSQL): def check(cls, user, key): "Check user key and delete old one" now = datetime.datetime.now() - timeout = datetime.timedelta(seconds=int(CONFIG['session_timeout'])) + timeout = datetime.timedelta( + seconds=config.getint('session', 'timeout')) sessions = cls.search([ ('create_uid', '=', user), ]) diff --git a/trytond/model/fields/binary.py b/trytond/model/fields/binary.py index 94b278e2b0..42faa79678 100644 --- a/trytond/model/fields/binary.py +++ b/trytond/model/fields/binary.py @@ -4,7 +4,7 @@ from sql import Query, Expression from .field import Field, SQLType from ...transaction import Transaction -from ...config import CONFIG +from ... import backend class Binary(Field): @@ -59,14 +59,14 @@ class Binary(Field): def sql_format(value): if isinstance(value, (Query, Expression)): return value - db_type = CONFIG['db_type'] + db_type = backend.name() if db_type == 'postgresql' and value is not None: import psycopg2 return psycopg2.Binary(value) return value def sql_type(self): - db_type = CONFIG['db_type'] + db_type = backend.name() if db_type == 'postgresql': return SQLType('BYTEA', 'BYTEA') elif db_type == 'mysql': diff --git a/trytond/model/fields/char.py b/trytond/model/fields/char.py index 1168cac80b..62540772cb 100644 --- a/trytond/model/fields/char.py +++ b/trytond/model/fields/char.py @@ -4,7 +4,7 @@ import warnings from sql import Query, Expression -from ...config import CONFIG +from ... import backend from .field import Field, FieldTranslate, size_validate, SQLType @@ -59,7 +59,7 @@ class Char(FieldTranslate): return value def sql_type(self): - db_type = CONFIG['db_type'] + db_type = backend.name() if self.size and db_type != 'sqlite': return SQLType('VARCHAR', 'VARCHAR(%s)' % self.size) elif db_type == 'mysql': diff --git a/trytond/model/fields/date.py b/trytond/model/fields/date.py index f769e9dd0c..428f6c4ee2 100644 --- a/trytond/model/fields/date.py +++ b/trytond/model/fields/date.py @@ -3,7 +3,7 @@ import datetime from sql import Query, Expression -from ...config import CONFIG +from ... import backend from .field import Field, SQLType @@ -71,7 +71,7 @@ class DateTime(Field): return value.replace(microsecond=0) def sql_type(self): - db_type = CONFIG['db_type'] + db_type = backend.name() if db_type == 'sqlite': return SQLType('TIMESTAMP', 'TIMESTAMP') elif db_type == 'mysql': @@ -106,7 +106,7 @@ class Timestamp(Field): return value def sql_type(self): - db_type = CONFIG['db_type'] + db_type = backend.name() if db_type == 'sqlite': return SQLType('TIMESTAMP', 'TIMESTAMP') elif db_type == 'mysql': diff --git a/trytond/model/fields/float.py b/trytond/model/fields/float.py index 1041f0a50c..42a8db50c8 100644 --- a/trytond/model/fields/float.py +++ b/trytond/model/fields/float.py @@ -2,7 +2,7 @@ #this repository contains the full copyright notices and license terms. from sql import Query, Expression -from ...config import CONFIG +from ... import backend from .field import Field, SQLType from ...pyson import PYSON @@ -59,7 +59,7 @@ class Float(Field): return float(value) def sql_type(self): - db_type = CONFIG['db_type'] + db_type = backend.name() if db_type == 'postgresql': return SQLType('FLOAT8', 'FLOAT8') elif db_type == 'mysql': diff --git a/trytond/model/fields/integer.py b/trytond/model/fields/integer.py index 2ba0694b23..64f51bf92e 100644 --- a/trytond/model/fields/integer.py +++ b/trytond/model/fields/integer.py @@ -2,7 +2,7 @@ #this repository contains the full copyright notices and license terms. from sql import Query, Expression -from ...config import CONFIG +from ... import backend from .field import Field, SQLType @@ -13,7 +13,7 @@ class Integer(Field): _type = 'integer' def sql_type(self): - db_type = CONFIG['db_type'] + db_type = backend.name() if db_type == 'postgresql': return SQLType('INT4', 'INT4') elif db_type == 'mysql': @@ -22,7 +22,7 @@ class Integer(Field): return SQLType('INTEGER', 'INTEGER') def sql_format(self, value): - db_type = CONFIG['db_type'] + db_type = backend.name() if (db_type == 'sqlite' and value is not None and not isinstance(value, (Query, Expression))): @@ -37,7 +37,7 @@ class BigInteger(Integer): _type = 'biginteger' def sql_type(self): - db_type = CONFIG['db_type'] + db_type = backend.name() if db_type == 'postgresql': return SQLType('INT8', 'INT8') return super(BigInteger, self).sql_type() diff --git a/trytond/model/fields/many2one.py b/trytond/model/fields/many2one.py index ef109d0084..e6aa9ba9db 100644 --- a/trytond/model/fields/many2one.py +++ b/trytond/model/fields/many2one.py @@ -6,7 +6,7 @@ from sql.operators import Or from .field import Field, SQLType from ...pool import Pool -from ...config import CONFIG +from ... import backend from ...tools import reduce_ids from ...transaction import Transaction @@ -86,7 +86,7 @@ class Many2One(Field): return int(value) def sql_type(self): - db_type = CONFIG['db_type'] + db_type = backend.name() if db_type == 'postgresql': return SQLType('INT4', 'INT4') elif db_type == 'mysql': diff --git a/trytond/model/fields/numeric.py b/trytond/model/fields/numeric.py index 2a8dda319c..3580bb7c66 100644 --- a/trytond/model/fields/numeric.py +++ b/trytond/model/fields/numeric.py @@ -3,7 +3,7 @@ from decimal import Decimal from sql import Query, Expression, Cast, Literal, Select, CombiningQuery -from ...config import CONFIG +from ... import backend from .field import SQLType from .float import Float @@ -26,14 +26,14 @@ class Numeric(Float): return value def sql_type(self): - db_type = CONFIG['db_type'] + db_type = backend.name() if db_type == 'mysql': return SQLType('DECIMAL', 'DECIMAL(65, 30)') return SQLType('NUMERIC', 'NUMERIC') def sql_column(self, table): column = super(Numeric, self).sql_column(table) - db_type = CONFIG['db_type'] + db_type = backend.name() if db_type == 'sqlite': # Must be casted as Decimal is stored as bytes column = Cast(column, self.sql_type().base) @@ -41,7 +41,7 @@ class Numeric(Float): def _domain_value(self, operator, value): value = super(Numeric, self)._domain_value(operator, value) - db_type = CONFIG['db_type'] + db_type = backend.name() if db_type == 'sqlite': if isinstance(value, (Select, CombiningQuery)): return value diff --git a/trytond/model/fields/reference.py b/trytond/model/fields/reference.py index 065248e3c8..6b941972e3 100644 --- a/trytond/model/fields/reference.py +++ b/trytond/model/fields/reference.py @@ -8,7 +8,7 @@ from .field import Field, SQLType from .char import Char from ...transaction import Transaction from ...pool import Pool -from ...config import CONFIG +from ... import backend class Reference(Field): @@ -109,7 +109,7 @@ class Reference(Field): return Char.sql_format(value) def sql_type(self): - db_type = CONFIG['db_type'] + db_type = backend.name() if db_type == 'mysql': return SQLType('CHAR', 'VARCHAR(255)') return SQLType('VARCHAR', 'VARCHAR') diff --git a/trytond/model/fields/selection.py b/trytond/model/fields/selection.py index 98fcb84542..1f75d2787c 100644 --- a/trytond/model/fields/selection.py +++ b/trytond/model/fields/selection.py @@ -4,7 +4,7 @@ import warnings from sql.conditionals import Case -from ...config import CONFIG +from ... import backend from .field import Field, SQLType @@ -44,7 +44,7 @@ class Selection(Field): __init__.__doc__ += Field.__init__.__doc__ def sql_type(self): - db_type = CONFIG['db_type'] + db_type = backend.name() if db_type == 'mysql': return SQLType('CHAR', 'VARCHAR(255)') return SQLType('VARCHAR', 'VARCHAR') diff --git a/trytond/model/fields/sha.py b/trytond/model/fields/sha.py index 0209337866..222aca836e 100644 --- a/trytond/model/fields/sha.py +++ b/trytond/model/fields/sha.py @@ -3,7 +3,7 @@ import hashlib from sql import Query, Expression -from ...config import CONFIG +from ... import backend from .field import SQLType from .char import Char @@ -22,7 +22,7 @@ class Sha(Char): return super(Sha, self).sql_format(value) def sql_type(self): - db_type = CONFIG['db_type'] + db_type = backend.name() if db_type == 'mysql': return SQLType('CHAR', 'VARCHAR(40)') return SQLType('VARCHAR', 'VARCHAR(40)') diff --git a/trytond/model/modelstorage.py b/trytond/model/modelstorage.py index 8ede941cf0..9564e402fb 100644 --- a/trytond/model/modelstorage.py +++ b/trytond/model/modelstorage.py @@ -24,7 +24,7 @@ from trytond.const import OPERATORS, RECORD_CACHE_SIZE, BROWSE_FIELD_TRESHOLD from trytond.transaction import Transaction from trytond.pool import Pool from trytond.cache import LRUDict, freeze -from trytond.config import CONFIG +from trytond import backend from trytond.rpc import RPC from .modelview import ModelView @@ -1018,7 +1018,7 @@ class ModelStorage(Model): if (value.quantize(Decimal(str(10.0 ** -digits[1]))) != value): raise_user_error(value) - elif CONFIG.options['db_type'] != 'mysql': + elif backend.name() != 'mysql': if not (round(value, digits[1]) == float(value)): raise_user_error(value) # validate digits diff --git a/trytond/modules/__init__.py b/trytond/modules/__init__.py index e7ee4062ed..b80920f6e8 100644 --- a/trytond/modules/__init__.py +++ b/trytond/modules/__init__.py @@ -14,7 +14,7 @@ from sql import Table from sql.functions import Now import trytond.tools as tools -from trytond.config import CONFIG +from trytond.config import config from trytond.transaction import Transaction from trytond.cache import Cache import trytond.convert as convert @@ -135,11 +135,11 @@ class Node(Singleton): def get_module_info(name): "Return the content of the tryton.cfg" - config = ConfigParser.ConfigParser() + module_config = ConfigParser.ConfigParser() with tools.file_open(os.path.join(name, 'tryton.cfg')) as fp: - config.readfp(fp) + module_config.readfp(fp) directory = os.path.dirname(fp.name) - info = dict(config.items('tryton')) + info = dict(module_config.items('tryton')) info['directory'] = directory for key in ('depends', 'extras_depend', 'xml'): if key in info: @@ -193,18 +193,17 @@ def create_graph(module_list): return graph, packages, later -def is_module_to_install(module): - for kind in ('init', 'update'): - if 'all' in CONFIG[kind] and module != 'tests': - return True - elif module in CONFIG[kind]: - return True +def is_module_to_install(module, update): + if 'all' in update and module != 'tests': + return True + elif module in update: + return True return False -def load_module_graph(graph, pool, lang=None): +def load_module_graph(graph, pool, update=None, lang=None): if lang is None: - lang = [CONFIG['language']] + lang = [config.get('database', 'language')] modules_todo = [] models_to_update_history = set() logger = logging.getLogger('modules') @@ -222,7 +221,7 @@ def load_module_graph(graph, pool, lang=None): logger.info(module) classes = pool.setup(module) package_state = module2state.get(module, 'uninstalled') - if (is_module_to_install(module) + if (is_module_to_install(module, update) or package_state in ('to install', 'to upgrade')): if package_state not in ('to install', 'to upgrade'): if package_state == 'installed': @@ -362,7 +361,7 @@ def register_classes(): MODULES.append(module) -def load_modules(database_name, pool, update=False, lang=None): +def load_modules(database_name, pool, update=None, lang=None): res = True def _load_modules(): @@ -372,7 +371,7 @@ def load_modules(database_name, pool, update=False, lang=None): # Migration from 2.2: workflow module removed cursor.execute(*ir_module.delete( where=(ir_module.name == 'workflow'))) - if 'all' in CONFIG['init']: + if 'all' in update: cursor.execute(*ir_module.select(ir_module.name, where=(ir_module.name != 'tests'))) else: @@ -385,16 +384,11 @@ def load_modules(database_name, pool, update=False, lang=None): 'to remove')))) module_list = [name for (name,) in cursor.fetchall()] if update: - for module in CONFIG['init'].keys(): - if CONFIG['init'][module]: - module_list.append(module) - for module in CONFIG['update'].keys(): - if CONFIG['update'][module]: - module_list.append(module) + module_list += update graph = create_graph(module_list)[0] try: - load_module_graph(graph, pool, lang) + load_module_graph(graph, pool, update, lang) except Exception: cursor.rollback() raise @@ -423,6 +417,7 @@ def load_modules(database_name, pool, update=False, lang=None): Module = pool.get('ir.module.module') Module.update_list() cursor.commit() + Cache.resets(database_name) if not Transaction().cursor: with Transaction().start(database_name, 0): @@ -433,5 +428,4 @@ def load_modules(database_name, pool, update=False, lang=None): Transaction().reset_context(): _load_modules() - Cache.resets(database_name) return res diff --git a/trytond/pool.py b/trytond/pool.py index f57f3dccde..65c6f49933 100644 --- a/trytond/pool.py +++ b/trytond/pool.py @@ -128,7 +128,7 @@ class Pool(object): ''' return self._locks[self.database_name] - def init(self, update=False, lang=None): + def init(self, update=None, lang=None): ''' Init pool Set update to proceed to update diff --git a/trytond/protocols/dispatcher.py b/trytond/protocols/dispatcher.py index 2d1915d302..f18b85ee2b 100644 --- a/trytond/protocols/dispatcher.py +++ b/trytond/protocols/dispatcher.py @@ -1,18 +1,17 @@ # -*- coding: utf-8 -*- #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. -import traceback import logging import time import sys import pydoc -from sql import Table, Flavor +from sql import Table from trytond.pool import Pool from trytond import security from trytond import backend -from trytond.config import CONFIG +from trytond.config import config from trytond.version import VERSION from trytond.transaction import Transaction from trytond.cache import Cache @@ -20,6 +19,7 @@ from trytond.exceptions import UserError, UserWarning, NotLogged, \ ConcurrencyException from trytond.rpc import RPC +logger = logging.getLogger(__name__) ir_configuration = Table('ir_configuration') ir_lang = Table('ir_lang') @@ -40,12 +40,13 @@ def dispatch(host, port, protocol, database_name, user, session, object_type, except Exception: return False res = security.login(database_name, user, session) - Cache.clean(database_name) + with Transaction().start(database_name, 0): + Cache.clean(database_name) + Cache.resets(database_name) logger = logging.getLogger('dispatcher') msg = res and 'successful login' or 'bad login or password' logger.info('%s \'%s\' from %s:%d using %s on database \'%s\'' % (msg, user, host, port, protocol, database_name)) - Cache.resets(database_name) return res or False elif method == 'logout': name = security.logout(database_name, user, session) @@ -81,7 +82,7 @@ def dispatch(host, port, protocol, database_name, user, session, object_type, except Exception: return False elif method == 'list': - if CONFIG['prevent_dblist']: + if config.get('database', 'list'): raise Exception('AccessDenied') with Transaction().start(None, 0, close=True) as transaction: return transaction.database.list(transaction.cursor) @@ -121,7 +122,7 @@ def dispatch(host, port, protocol, database_name, user, session, object_type, obj = pool.get(object_name, type=object_type) return pydoc.getdoc(getattr(obj, method)) - for count in range(int(CONFIG['retry']), -1, -1): + for count in range(config.getint('database', 'retry'), -1, -1): try: user = security.check(database_name, user, session) except DatabaseOperationalError: @@ -130,7 +131,6 @@ def dispatch(host, port, protocol, database_name, user, session, object_type, raise break - Cache.clean(database_name) database_list = Pool.database_list() pool = Pool(database_name) if not database_name in database_list: @@ -147,9 +147,13 @@ def dispatch(host, port, protocol, database_name, user, session, object_type, raise UserError('Calling method %s on %s %s is not allowed!' % (method, object_type, object_name)) - for count in range(int(CONFIG['retry']), -1, -1): + exception_message = ('Exception calling %s.%s.%s from %s@%s:%d/%s' % + (object_type, object_name, method, user, host, port, database_name)) + + for count in range(config.getint('database', 'retry'), -1, -1): with Transaction().start(database_name, user, readonly=rpc.readonly) as transaction: + Cache.clean(database_name) try: c_args, c_kwargs, transaction.context, transaction.timestamp \ = rpc.convert(obj, *args, **kwargs) @@ -166,23 +170,20 @@ def dispatch(host, port, protocol, database_name, user, session, object_type, for i in inst] if not rpc.readonly: transaction.cursor.commit() - except DatabaseOperationalError, exception: + except DatabaseOperationalError: transaction.cursor.rollback() if count and not rpc.readonly: continue raise - except Exception, exception: - if CONFIG['verbose'] and not isinstance(exception, ( - NotLogged, ConcurrencyException, UserError, - UserWarning)): - tb_s = ''.join(traceback.format_exception(*sys.exc_info())) - logger = logging.getLogger('dispatcher') - logger.error('Exception calling method %s on ' - '%s %s from %s@%s:%d/%s:\n' - % (method, object_type, object_name, user, host, port, - database_name) + tb_s) + except (NotLogged, ConcurrencyException, UserError, UserWarning): + logger.debug(exception_message, exc_info=sys.exc_info()) transaction.cursor.rollback() raise + except Exception: + logger.error(exception_message, exc_info=sys.exc_info()) + transaction.cursor.rollback() + raise + Cache.resets(database_name) with Transaction().start(database_name, 0) as transaction: pool = Pool(database_name) Session = pool.get('ir.session') @@ -193,7 +194,6 @@ def dispatch(host, port, protocol, database_name, user, session, object_type, transaction.cursor.rollback() else: transaction.cursor.commit() - Cache.resets(database_name) return result @@ -225,7 +225,7 @@ def create(database_name, password, lang, admin_password): transaction.cursor.commit() pool = Pool(database_name) - pool.init(update=True, lang=[lang]) + pool.init(update=['res', 'ir'], lang=[lang]) with Transaction().start(database_name, 0) as transaction: User = pool.get('res.user') Lang = pool.get('ir.lang') @@ -246,9 +246,8 @@ def create(database_name, password, lang, admin_password): transaction.cursor.commit() res = True except Exception: - logger.error('CREATE DB: %s failed' % (database_name,)) - tb_s = ''.join(traceback.format_exception(*sys.exc_info())) - logger.error('Exception in call: \n' + tb_s) + logger.error('CREATE DB: %s failed' % database_name, + exc_info=sys.exc_info()) raise else: logger.info('CREATE DB: %s' % (database_name,)) @@ -270,9 +269,8 @@ def drop(database_name, password): Database.drop(cursor, database_name) cursor.commit() except Exception: - logger.error('DROP DB: %s failed' % (database_name,)) - tb_s = ''.join(traceback.format_exception(*sys.exc_info())) - logger.error('Exception in call: \n' + tb_s) + logger.error('DROP DB: %s failed' % database_name, + exc_info=sys.exc_info()) raise else: logger.info('DROP DB: %s' % (database_name)) diff --git a/trytond/protocols/jsonrpc.py b/trytond/protocols/jsonrpc.py index 62adae83d9..088d7da5e0 100644 --- a/trytond/protocols/jsonrpc.py +++ b/trytond/protocols/jsonrpc.py @@ -2,7 +2,7 @@ #this repository contains the full copyright notices and license terms. from trytond.protocols.sslsocket import SSLSocket from trytond.protocols.dispatcher import dispatch -from trytond.config import CONFIG +from trytond.config import config from trytond.protocols.common import daemon, RegisterHandlerMixin from trytond.exceptions import UserError, UserWarning, NotLogged, \ ConcurrencyException @@ -157,10 +157,6 @@ class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher): tb_s = ''.join(traceback.format_exception(*sys.exc_info())) for path in sys.path: tb_s = tb_s.replace(path, '') - if CONFIG['debug_mode']: - import pdb - traceb = sys.exc_info()[2] - pdb.post_mortem(traceb) # report exception back to server response['error'] = (str(sys.exc_value), tb_s) @@ -229,7 +225,7 @@ class SimpleJSONRPCRequestHandler(RegisterHandlerMixin, path = posixpath.normpath(urllib.unquote(path)) words = path.split('/') words = filter(None, words) - path = CONFIG['jsondata_path'] + path = config.get('jsonrpc', 'data') for word in words: drive, word = os.path.splitdrive(word) head, word = os.path.split(word) @@ -247,7 +243,8 @@ class SimpleJSONRPCRequestHandler(RegisterHandlerMixin, def send_tryton_url(self, path): self.send_response(300) - hostname = CONFIG['hostname'] or unicode(socket.getfqdn(), 'utf8') + hostname = (config.get('jsonrpc', 'hostname') + or unicode(socket.getfqdn(), 'utf8')) hostname = '.'.join(encodings.idna.ToASCII(part) for part in hostname.split('.')) values = { diff --git a/trytond/protocols/sslsocket.py b/trytond/protocols/sslsocket.py index c3dcbdbe5c..9c18a26347 100644 --- a/trytond/protocols/sslsocket.py +++ b/trytond/protocols/sslsocket.py @@ -1,6 +1,6 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. -from trytond.config import CONFIG +from trytond.config import config def SSLSocket(socket): @@ -8,6 +8,6 @@ def SSLSocket(socket): import ssl return ssl.wrap_socket(socket, server_side=True, - certfile=CONFIG['certificate'], - keyfile=CONFIG['privatekey'], + certfile=config.get('ssl', 'certificate'), + keyfile=config.get('ssl', 'privatekey'), ssl_version=ssl.PROTOCOL_SSLv23) diff --git a/trytond/protocols/webdav.py b/trytond/protocols/webdav.py index 98b12ff00e..a4a38df15d 100644 --- a/trytond/protocols/webdav.py +++ b/trytond/protocols/webdav.py @@ -7,7 +7,6 @@ import urlparse import time import urllib import sys -import traceback import logging from threading import local import xml.dom.minidom @@ -21,7 +20,6 @@ from pywebdav.lib.davcmd import copyone, copytree, moveone, movetree, \ delone, deltree from trytond.protocols.sslsocket import SSLSocket from trytond.protocols.common import daemon -from trytond.config import CONFIG from trytond.security import login from trytond.version import PACKAGE, VERSION, WEBSITE from trytond.tools.misc import LocalDict @@ -36,6 +34,8 @@ domimpl = xml.dom.minidom.getDOMImplementation() DAV_VERSION_1['version'] += ',access-control' DAV_VERSION_2['version'] += ',access-control' +logger = logging.getLogger(__name__) + # Local int for multi-thread class LocalInt(local): @@ -131,12 +131,12 @@ class TrytonDAVInterface(iface.dav_interface): self.verbose = False def _log_exception(self, exception): - if CONFIG['verbose'] and not isinstance(exception, ( - NotLogged, ConcurrencyException, UserError, UserWarning, - DAV_Error, DAV_NotFound, DAV_Secret, DAV_Forbidden)): - tb_s = ''.join(traceback.format_exception(*sys.exc_info())) - logger = logging.getLogger('webdav') - logger.error('Exception:\n' + tb_s) + if isinstance(exception, (NotLogged, ConcurrencyException, UserError, + UserWarning, DAV_Error, DAV_NotFound, DAV_Secret, + DAV_Forbidden)): + logger.debug('Exception', exc_info=sys.exc_info()) + else: + logger.error('Exception', exc_info=sys.exc_info()) @staticmethod def get_dburi(uri): diff --git a/trytond/protocols/xmlrpc.py b/trytond/protocols/xmlrpc.py index 4bc20d18ea..7b48c5358d 100644 --- a/trytond/protocols/xmlrpc.py +++ b/trytond/protocols/xmlrpc.py @@ -2,7 +2,6 @@ #this repository contains the full copyright notices and license terms. from trytond.protocols.sslsocket import SSLSocket from trytond.protocols.dispatcher import dispatch -from trytond.config import CONFIG from trytond.protocols.common import daemon, RegisterHandlerMixin from trytond.exceptions import UserError, UserWarning, NotLogged, \ ConcurrencyException @@ -10,16 +9,18 @@ from trytond import security import SimpleXMLRPCServer import SocketServer import xmlrpclib -import traceback import socket import sys import base64 import datetime from types import DictType +import logging # convert decimal to float before marshalling: from decimal import Decimal +logger = logging.getLogger(__name__) + def dump_decimal(self, value, write): value = {'__class__': 'Decimal', @@ -133,6 +134,7 @@ class GenericXMLRPCRequestHandler: database_name = self.path[1:] user = self.tryton['user'] session = self.tryton['session'] + exception_message = 'Exception calling %s%s' % (method, params) try: try: method_list = method.split('.') @@ -150,21 +152,15 @@ class GenericXMLRPCRequestHandler: return dispatch(host, port, 'XML-RPC', database_name, user, session, object_type, object_name, method, *params) except (NotLogged, ConcurrencyException), exception: - raise xmlrpclib.Fault(exception.code, - '\n'.join(exception.args)) + logger.debug(exception_message, exc_info=sys.exc_info()) + raise xmlrpclib.Fault(exception.code, str(exception)) except (UserError, UserWarning), exception: + logger.debug(exception_message, exc_info=sys.exc_info()) error, description = exception.args - raise xmlrpclib.Fault(exception.code, - '\n'.join((error,) + description)) - except Exception: - tb_s = ''.join(traceback.format_exception(*sys.exc_info())) - for path in sys.path: - tb_s = tb_s.replace(path, '') - if CONFIG['debug_mode']: - import pdb - traceb = sys.exc_info()[2] - pdb.post_mortem(traceb) - raise xmlrpclib.Fault(255, str(sys.exc_value) + '\n' + tb_s) + raise xmlrpclib.Fault(exception.code, str(exception)) + except Exception, exception: + logger.error(exception_message, exc_info=sys.exc_info()) + raise xmlrpclib.Fault(255, str(exception)) finally: security.logout(database_name, user, session) diff --git a/trytond/report/report.py b/trytond/report/report.py index 42a6d5d03d..a3aadc9d27 100644 --- a/trytond/report/report.py +++ b/trytond/report/report.py @@ -21,7 +21,7 @@ except ImportError: Manifest, MANIFEST = None, None from genshi.filters import Translator import lxml.etree -from trytond.config import CONFIG +from trytond.config import config from trytond.pool import Pool, PoolBase from trytond.transaction import Transaction from trytond.url import URLMixin @@ -295,7 +295,7 @@ class Report(URLMixin, PoolBase): oext = FORMAT2EXT.get(output_format, output_format) with os.fdopen(fd, 'wb+') as fp: fp.write(data) - cmd = ['unoconv', '--connection=%s' % CONFIG['unoconv'], + cmd = ['unoconv', '--connection=%s' % config.get('report', 'unoconv'), '-f', oext, '--stdout', path] try: proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) diff --git a/trytond/res/user.py b/trytond/res/user.py index 8c0fbb720f..f5892c1727 100644 --- a/trytond/res/user.py +++ b/trytond/res/user.py @@ -26,7 +26,7 @@ from .. import backend from ..transaction import Transaction from ..cache import Cache from ..pool import Pool -from ..config import CONFIG +from ..config import config from ..pyson import PYSONEncoder from ..rpc import RPC @@ -197,7 +197,8 @@ class User(ModelSQL, ModelView): def get_sessions(users, name): Session = Pool().get('ir.session') now = datetime.datetime.now() - timeout = datetime.timedelta(seconds=int(CONFIG['session_timeout'])) + timeout = datetime.timedelta( + seconds=config.getint('session', 'timeout')) result = dict((u.id, 0) for u in users) for sub_ids in grouped_slice(users): with Transaction().set_user(0): @@ -553,7 +554,7 @@ class LoginAttempt(ModelSQL): @staticmethod def delay(): return (datetime.datetime.now() - - datetime.timedelta(seconds=int(CONFIG['session_timeout']))) + - datetime.timedelta(seconds=config.getint('session', 'timeout'))) @classmethod def add(cls, login): diff --git a/trytond/security.py b/trytond/security.py index 7d3e85bc32..4e92b0053e 100644 --- a/trytond/security.py +++ b/trytond/security.py @@ -1,7 +1,13 @@ #This file is part of Tryton. The COPYRIGHT file at the top level of #this repository contains the full copyright notices and license terms. +import os +try: + import crypt +except ImportError: + pass + from trytond.pool import Pool -from trytond.config import CONFIG +from trytond.config import config from trytond.transaction import Transaction from trytond.exceptions import NotLogged @@ -48,10 +54,10 @@ def logout(dbname, user, session): def check_super(passwd): - if passwd == CONFIG['admin_passwd']: + cryptedpasswd = config.get('session', 'super_pwd') + if cryptedpasswd and crypt.crypt(passwd, cryptedpasswd) == cryptedpasswd: return True - else: - raise Exception('AccessDenied') + raise Exception('AccessDenied') def check(dbname, user, session): diff --git a/trytond/server.py b/trytond/server.py index ee978ca625..992e08a25f 100644 --- a/trytond/server.py +++ b/trytond/server.py @@ -13,7 +13,7 @@ import time from getpass import getpass import threading -from trytond.config import CONFIG +from trytond.config import config, parse_listen from trytond import backend from trytond.pool import Pool from trytond.monitor import monitor @@ -24,13 +24,12 @@ class TrytonServer(object): def __init__(self, options): - CONFIG.update_etc(options['configfile']) - CONFIG.update_cmdline(options) + config.update_etc(options.configfile) - if CONFIG['logconf']: - logging.config.fileConfig(CONFIG['logconf']) + if options.logconf: + logging.config.fileConfig(options.logconf) logging.getLogger('server').info('using %s as logging ' - 'configuration file', CONFIG['logconf']) + 'configuration file', options.logconf) else: logformat = '[%(asctime)s] %(levelname)s:%(name)s:%(message)s' datefmt = '%a %b %d %H:%M:%S %Y' @@ -39,19 +38,20 @@ class TrytonServer(object): self.logger = logging.getLogger('server') - if CONFIG.configfile: + if options.configfile: self.logger.info('using %s as configuration file' - % CONFIG.configfile) + % options.configfile) else: self.logger.info('using default configuration') self.logger.info('initialising distributed objects services') self.xmlrpcd = [] self.jsonrpcd = [] self.webdavd = [] + self.options = options def run(self): "Run the server and never return" - update = bool(CONFIG['init'] or CONFIG['update']) + update = self.options.init + self.options.update init = {} signal.signal(signal.SIGINT, lambda *a: self.stop()) @@ -61,22 +61,18 @@ class TrytonServer(object): if hasattr(signal, 'SIGUSR1'): signal.signal(signal.SIGUSR1, lambda *a: self.restart()) - if CONFIG['pidfile']: - with open(CONFIG['pidfile'], 'w') as fd_pid: + if self.options.pidfile: + with open(self.options.pidfile, 'w') as fd_pid: fd_pid.write("%d" % (os.getpid())) - if not CONFIG["db_name"] \ - and bool(CONFIG['init'] or CONFIG['update']): - raise Exception('Missing database option!') - if not update: self.start_servers() - for db_name in CONFIG["db_name"]: + for db_name in self.options.database_names: init[db_name] = False with Transaction().start(db_name, 0) as transaction: cursor = transaction.cursor - if CONFIG['init']: + if self.options.init: if not cursor.test(): self.logger.info("init db") backend.get('Database').init(cursor) @@ -85,7 +81,7 @@ class TrytonServer(object): elif not cursor.test(): raise Exception("'%s' is not a Tryton database!" % db_name) - for db_name in CONFIG["db_name"]: + for db_name in self.options.database_names: if update: with Transaction().start(db_name, 0) as transaction: cursor = transaction.cursor @@ -99,10 +95,7 @@ class TrytonServer(object): lang = None Pool(db_name).init(update=update, lang=lang) - for kind in ('init', 'update'): - CONFIG[kind] = {} - - for db_name in CONFIG['db_name']: + for db_name in self.options.database_names: if init[db_name]: # try to read password from environment variable # TRYTONPASSFILE, empty TRYTONPASSFILE ignored @@ -145,7 +138,7 @@ class TrytonServer(object): threads = {} while True: - if CONFIG['cron']: + if self.options.cron: for dbname in Pool.database_list(): thread = threads.get(dbname) if thread and thread.is_alive(): @@ -164,7 +157,7 @@ class TrytonServer(object): args=(dbname,), kwargs={}) thread.start() threads[dbname] = thread - if CONFIG['auto_reload']: + if self.options.debug: for _ in range(60): if monitor(): self.restart() @@ -173,33 +166,31 @@ class TrytonServer(object): time.sleep(60) def start_servers(self): + ssl = config.get('ssl', 'privatekey') # Launch Server - if CONFIG['jsonrpc']: + if config.get('jsonrpc', 'listen'): from trytond.protocols.jsonrpc import JSONRPCDaemon - for hostname, port in CONFIG['jsonrpc']: - self.jsonrpcd.append(JSONRPCDaemon(hostname, port, - CONFIG['ssl_jsonrpc'])) + for hostname, port in parse_listen( + config.get('jsonrpc', 'listen')): + self.jsonrpcd.append(JSONRPCDaemon(hostname, port, ssl)) self.logger.info("starting JSON-RPC%s protocol on %s:%d" % - (CONFIG['ssl_jsonrpc'] and ' SSL' or '', hostname or '*', - port)) + (ssl and ' SSL' or '', hostname or '*', port)) - if CONFIG['xmlrpc']: + if config.get('xmlrpc', 'listen'): from trytond.protocols.xmlrpc import XMLRPCDaemon - for hostname, port in CONFIG['xmlrpc']: - self.xmlrpcd.append(XMLRPCDaemon(hostname, port, - CONFIG['ssl_xmlrpc'])) + for hostname, port in parse_listen( + config.get('xmlrpc', 'listen')): + self.xmlrpcd.append(XMLRPCDaemon(hostname, port, ssl)) self.logger.info("starting XML-RPC%s protocol on %s:%d" % - (CONFIG['ssl_xmlrpc'] and ' SSL' or '', hostname or '*', - port)) + (ssl and ' SSL' or '', hostname or '*', port)) - if CONFIG['webdav']: + if config.get('webdav', 'listen'): from trytond.protocols.webdav import WebDAVServerThread - for hostname, port in CONFIG['webdav']: - self.webdavd.append(WebDAVServerThread(hostname, port, - CONFIG['ssl_webdav'])) + for hostname, port in parse_listen( + config.get('webdav', 'listen')): + self.webdavd.append(WebDAVServerThread(hostname, port, ssl)) self.logger.info("starting WebDAV%s protocol on %s:%d" % - (CONFIG['ssl_webdav'] and ' SSL' or '', hostname or '*', - port)) + (ssl and ' SSL' or '', hostname or '*', port)) for servers in (self.xmlrpcd, self.jsonrpcd, self.webdavd): for server in servers: @@ -211,8 +202,8 @@ class TrytonServer(object): server.stop() server.join() if exit: - if CONFIG['pidfile']: - os.unlink(CONFIG['pidfile']) + if self.options.pidfile: + os.unlink(self.options.pidfile) logging.getLogger('server').info('stopped') logging.shutdown() sys.exit(0) diff --git a/trytond/tests/run-tests.py b/trytond/tests/run-tests.py index f576c34575..f6dd242442 100755 --- a/trytond/tests/run-tests.py +++ b/trytond/tests/run-tests.py @@ -8,7 +8,8 @@ import time import unittest import sys -from trytond.config import CONFIG +from trytond.config import config +from trytond import backend if __name__ != '__main__': raise ImportError('%s can not be imported' % __name__) @@ -22,18 +23,17 @@ parser.add_argument("-m", "--modules", action="store_true", dest="modules", parser.add_argument("-v", action="count", default=0, dest="verbosity", help="Increase verbosity") parser.add_argument('tests', metavar='test', nargs='*') +parser.epilog = ('The database name can be specified in the DB_NAME ' + 'environment variable.') opt = parser.parse_args() -CONFIG['db_type'] = 'sqlite' -CONFIG.update_etc(opt.config) -if not CONFIG['admin_passwd']: - CONFIG['admin_passwd'] = 'admin' +config.update_etc(opt.config) -if CONFIG['db_type'] == 'sqlite': +if backend.name() == 'sqlite': database_name = ':memory:' else: database_name = 'test_' + str(int(time.time())) -os.environ['DB_NAME'] = database_name +os.environ.setdefault('DB_NAME', database_name) from trytond.tests.test_tryton import all_suite, modules_suite if not opt.modules: diff --git a/trytond/tests/test_history.py b/trytond/tests/test_history.py index 3a8ccfcd3e..25a9adfb00 100644 --- a/trytond/tests/test_history.py +++ b/trytond/tests/test_history.py @@ -7,7 +7,7 @@ from trytond.tests.test_tryton import POOL, DB_NAME, USER, CONTEXT, \ install_module from trytond.transaction import Transaction from trytond.exceptions import UserError -from trytond.config import CONFIG +from trytond import backend class HistoryTestCase(unittest.TestCase): @@ -76,7 +76,7 @@ class HistoryTestCase(unittest.TestCase): with Transaction().set_context(_datetime=datetime.datetime.min): self.assertRaises(UserError, History.read, [history_id]) - @unittest.skipIf(CONFIG['db_type'] in ('sqlite', 'mysql'), + @unittest.skipIf(backend.name() in ('sqlite', 'mysql'), 'now() is not the start of the transaction') def test0020read_same_timestamp(self): 'Test read history with same timestamp' @@ -184,7 +184,7 @@ class HistoryTestCase(unittest.TestCase): History.restore_history([history_id], datetime.datetime.min) self.assertRaises(UserError, History.read, [history_id]) - @unittest.skipIf(CONFIG['db_type'] in ('sqlite', 'mysql'), + @unittest.skipIf(backend.name() in ('sqlite', 'mysql'), 'now() is not the start of the transaction') def test0045restore_history_same_timestamp(self): 'Test restore history with same timestamp' @@ -287,7 +287,7 @@ class HistoryTestCase(unittest.TestCase): records = History.search([], order=order) self.assertEqual(records, instances) - @unittest.skipIf(CONFIG['db_type'] in ('sqlite', 'mysql'), + @unittest.skipIf(backend.name() in ('sqlite', 'mysql'), 'now() is not the start of the transaction') def test0060_ordered_search_same_timestamp(self): 'Test ordered search with same timestamp' diff --git a/trytond/tests/test_modelsql.py b/trytond/tests/test_modelsql.py index 0fa039b3e8..90356570c0 100644 --- a/trytond/tests/test_modelsql.py +++ b/trytond/tests/test_modelsql.py @@ -5,7 +5,7 @@ import unittest import time -from trytond.config import CONFIG +from trytond import backend from trytond.exceptions import UserError, ConcurrencyException from trytond.transaction import Transaction from trytond.tests.test_tryton import POOL, DB_NAME, USER, CONTEXT, \ @@ -20,7 +20,7 @@ class ModelSQLTestCase(unittest.TestCase): self.modelsql = POOL.get('test.modelsql') self.modelsql_timestamp = POOL.get('test.modelsql.timestamp') - @unittest.skipIf(CONFIG['db_type'] == 'sqlite', + @unittest.skipIf(backend.name == 'sqlite', 'SQLite not concerned because tryton don\'t set "NOT NULL"' 'constraint: "ALTER TABLE" don\'t support NOT NULL constraint' 'without default value') @@ -55,7 +55,7 @@ class ModelSQLTestCase(unittest.TestCase): timestamp = self.modelsql_timestamp.read([record.id], ['_timestamp'])[0]['_timestamp'] - if CONFIG['db_type'] in ('sqlite', 'mysql'): + if backend.name() in ('sqlite', 'mysql'): # timestamp precision of sqlite is the second time.sleep(1) diff --git a/trytond/tests/test_tryton.py b/trytond/tests/test_tryton.py index 048f46aa84..94bfab9bd2 100644 --- a/trytond/tests/test_tryton.py +++ b/trytond/tests/test_tryton.py @@ -7,16 +7,17 @@ import unittest import doctest from lxml import etree -from trytond.config import CONFIG from trytond.pool import Pool from trytond import backend -from trytond.protocols.dispatcher import create +from trytond.protocols.dispatcher import create, drop from trytond.transaction import Transaction from trytond.pyson import PYSONEncoder, Eval from trytond.exceptions import UserError +from trytond import security __all__ = ['POOL', 'DB_NAME', 'USER', 'USER_PASSWORD', 'CONTEXT', - 'install_module', 'test_view', 'test_depends', 'doctest_dropdb', + 'install_module', 'test_view', 'test_depends', + 'doctest_setup', 'doctest_teardown', 'suite', 'all_suite', 'modules_suite'] Pool.start() @@ -27,6 +28,7 @@ DB_NAME = os.environ['DB_NAME'] DB = backend.get('Database')(DB_NAME) Pool.test = True POOL = Pool(DB_NAME) +security.check_super = lambda *a, **k: True class ModelViewTestCase(unittest.TestCase): @@ -72,13 +74,7 @@ def install_module(name): ''' Install module for the tested database ''' - Database = backend.get('Database') - database = Database().connect() - cursor = database.cursor() - databases = database.list(cursor) - cursor.close() - if DB_NAME not in databases: - create(DB_NAME, CONFIG['admin_passwd'], 'en_US', USER_PASSWORD) + create_db() with Transaction().start(DB_NAME, USER, context=CONTEXT) as transaction: Module = POOL.get('ir.module.module') @@ -177,16 +173,32 @@ def test_depends(): list(depends - set(model._fields)), mname, fname)) -def doctest_dropdb(test): - '''Remove SQLite memory database''' - from trytond.backend.sqlite.database import Database as SQLiteDatabase - database = SQLiteDatabase().connect() - cursor = database.cursor(autocommit=True) - try: - SQLiteDatabase.drop(cursor, ':memory:') - cursor.commit() - finally: - cursor.close() +def db_exist(): + Database = backend.get('Database') + database = Database().connect() + cursor = database.cursor() + databases = database.list(cursor) + cursor.close() + return DB_NAME in databases + + +def create_db(): + if not db_exist(): + create(DB_NAME, None, 'en_US', USER_PASSWORD) + + +def drop_db(): + if db_exist(): + drop(DB_NAME, None) + + +def drop_create(): + if db_exist: + drop_db() + create_db() + +doctest_setup = lambda test: drop_create() +doctest_teardown = lambda test: drop_db() def suite(): diff --git a/trytond/tools/misc.py b/trytond/tools/misc.py index 8dcdca1aa3..481d2169a0 100644 --- a/trytond/tools/misc.py +++ b/trytond/tools/misc.py @@ -15,8 +15,9 @@ from array import array from itertools import islice from sql import Literal from sql.operators import Or -from trytond.config import CONFIG + from trytond.const import OPERATORS +from trytond.config import config, parse_uri from trytond.transaction import Transaction @@ -34,42 +35,7 @@ def find_in_path(name): return name -def find_pg_tool(name): - if CONFIG['pg_path'] and CONFIG['pg_path'] != 'None': - return os.path.join(CONFIG['pg_path'], name) - else: - return find_in_path(name) - - -def exec_pg_command(name, *args): - prog = find_pg_tool(name) - if not prog: - raise Exception('Couldn\'t find %s' % name) - args2 = (os.path.basename(prog),) + args - return os.spawnv(os.P_WAIT, prog, args2) - - -def exec_pg_command_pipe(name, *args): - prog = find_pg_tool(name) - if not prog: - raise Exception('Couldn\'t find %s' % name) - if os.name == "nt": - cmd = '"' + prog + '" ' + ' '.join(args) - else: - cmd = prog + ' ' + ' '.join(args) - - # if db_password is set in configuration we should pass - # an environment variable PGPASSWORD to our subprocess - # see libpg documentation - child_env = dict(os.environ) - if CONFIG['db_password']: - child_env['PGPASSWORD'] = CONFIG['db_password'] - pipe = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, env=child_env) - return pipe - - -def exec_command_pipe(name, *args): +def exec_command_pipe(name, *args, **kwargs): prog = find_in_path(name) if not prog: raise Exception('Couldn\'t find %s' % name) @@ -77,7 +43,11 @@ def exec_command_pipe(name, *args): cmd = '"' + prog + '" ' + ' '.join(args) else: cmd = prog + ' ' + ' '.join(args) - return os.popen2(cmd, 'b') + child_env = dict(os.environ) + if kwargs.get('env'): + child_env.update(kwargs['env']) + return subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, env=child_env) def file_open(name, mode="r", subdir='modules'): @@ -134,17 +104,17 @@ def get_smtp_server(): :return: A SMTP instance. The quit() method must be call when all the calls to sendmail() have been made. """ - if CONFIG['smtp_ssl']: - smtp_server = smtplib.SMTP_SSL(CONFIG['smtp_server'], - CONFIG['smtp_port']) + uri = parse_uri(config.get('email', 'uri')) + if uri.scheme.startswith('smtps'): + smtp_server = smtplib.SMTP_SSL(uri.hostname, uri.port) else: - smtp_server = smtplib.SMTP(CONFIG['smtp_server'], CONFIG['smtp_port']) + smtp_server = smtplib.SMTP(uri.hostname, uri.port) - if CONFIG['smtp_tls']: + if 'tls' in uri.scheme: smtp_server.starttls() - if CONFIG['smtp_user'] and CONFIG['smtp_password']: - smtp_server.login(CONFIG['smtp_user'], CONFIG['smtp_password']) + if uri.username and uri.password: + smtp_server.login(uri.username, uri.password) return smtp_server diff --git a/trytond/url.py b/trytond/url.py index ce6dd11e47..be17878c0c 100644 --- a/trytond/url.py +++ b/trytond/url.py @@ -5,12 +5,12 @@ import encodings.idna import urllib import socket -from trytond.config import CONFIG +from trytond.config import config from trytond.transaction import Transaction __all__ = ['URLMixin'] -HOSTNAME = (CONFIG['hostname_jsonrpc'] +HOSTNAME = (config.get('jsonrpc', 'hostname') or unicode(socket.getfqdn(), 'utf8')) HOSTNAME = '.'.join(encodings.idna.ToASCII(part) if part else '' for part in HOSTNAME.split('.')) diff --git a/trytond/webdav/webdav.py b/trytond/webdav/webdav.py index 2b9e6e1984..bc7151d40e 100644 --- a/trytond/webdav/webdav.py +++ b/trytond/webdav/webdav.py @@ -16,7 +16,7 @@ from trytond.model import ModelView, ModelSQL, fields from trytond.tools import reduce_ids, safe_eval from trytond.transaction import Transaction from trytond.pool import Pool -from trytond.config import CONFIG +from trytond.config import config from trytond.pyson import Eval from trytond.tools import grouped_slice @@ -26,11 +26,11 @@ __all__ = [ def get_webdav_url(): - if CONFIG['ssl_webdav']: + if config.get('ssl', 'privatekey'): protocol = 'https' else: protocol = 'http' - hostname = (CONFIG['hostname_webdav'] + hostname = (config.get('webdav', 'hostname') or unicode(socket.getfqdn(), 'utf8')) hostname = '.'.join(encodings.idna.ToASCII(part) for part in hostname.split('.'))