[IMP] module.loading: attempt to load each module atomically within load_module_graph...
[odoo/odoo.git] / openerp / modules / loading.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #    Copyright (C) 2010-2011 OpenERP s.a. (<http://openerp.com>).
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU Affero General Public License as
10 #    published by the Free Software Foundation, either version 3 of the
11 #    License, or (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU Affero General Public License for more details.
17 #
18 #    You should have received a copy of the GNU Affero General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23 """ Modules (also called addons) management.
24
25 """
26
27 import os, sys, imp
28 from os.path import join as opj
29 import itertools
30 import zipimport
31
32 import openerp
33
34 import openerp.osv as osv
35 import openerp.tools as tools
36 import openerp.tools.osutil as osutil
37 from openerp.tools.safe_eval import safe_eval as eval
38 import openerp.pooler as pooler
39 from openerp.tools.translate import _
40
41 import openerp.netsvc as netsvc
42
43 import zipfile
44 import openerp.release as release
45
46 import re
47 import base64
48 from zipfile import PyZipFile, ZIP_DEFLATED
49 from cStringIO import StringIO
50
51 import logging
52
53 import openerp.modules.db
54 import openerp.modules.graph
55 import openerp.modules.migration
56
57 from openerp.modules.module import \
58     get_modules, get_modules_with_version, \
59     load_information_from_description_file, \
60     get_module_resource, zip_directory, \
61     get_module_path, initialize_sys_path, \
62     register_module_classes, init_module_models
63
64 logger = netsvc.Logger()
65
66
67 def open_openerp_namespace():
68     # See comment for open_openerp_namespace.
69     if openerp.conf.deprecation.open_openerp_namespace:
70         for k, v in list(sys.modules.items()):
71             if k.startswith('openerp.') and sys.modules.get(k[8:]) is None:
72                 sys.modules[k[8:]] = v
73
74
75 def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=None, report=None):
76     """Migrates+Updates or Installs all module nodes from ``graph``
77        :param graph: graph of module nodes to load
78        :param status: status dictionary for keeping track of progress
79        :param perform_checks: whether module descriptors should be checked for validity (prints warnings
80                               for same cases, and even raise osv_except if certificate is invalid)
81        :param skip_modules: optional list of module names (packages) which have previously been loaded and can be skipped
82        :return: list of modules that were installed or updated
83     """
84     def process_sql_file(cr, fp):
85         queries = fp.read().split(';')
86         for query in queries:
87             new_query = ' '.join(query.split())
88             if new_query:
89                 cr.execute(new_query)
90
91     def load_init_xml(cr, module_name, idref, mode):
92         _load_data(cr, module_name, idref, mode, 'init_xml')
93
94     def load_update_xml(cr, module_name, idref, mode):
95         _load_data(cr, module_name, idref, mode, 'update_xml')
96
97     def load_demo_xml(cr, module_name, idref, mode):
98         _load_data(cr, module_name, idref, mode, 'demo_xml')
99
100     def load_data(cr, module_name, idref, mode):
101         _load_data(cr, module_name, idref, mode, 'data')
102
103     def load_demo(cr, module_name, idref, mode):
104         _load_data(cr, module_name, idref, mode, 'demo')
105
106     def load_test(cr, module_name, idref, mode):
107         cr.commit()
108         if not tools.config.options['test_disable']:
109             try:
110                 _load_data(cr, module_name, idref, mode, 'test')
111             except Exception, e:
112                 logging.getLogger('test').exception('Tests failed to execute in module %s', module_name)
113             finally:
114                 if tools.config.options['test_commit']:
115                     cr.commit()
116                 else:
117                     cr.rollback()
118
119     def _load_data(cr, module_name, idref, mode, kind):
120         """
121
122         kind: data, demo, test, init_xml, update_xml, demo_xml.
123
124         noupdate is False, unless it is demo data or it is csv data in
125         init mode.
126
127         """
128         for filename in package.data[kind]:
129             log = logging.getLogger('init')
130             log.info("module %s: loading %s", module_name, filename)
131             _, ext = os.path.splitext(filename)
132             pathname = os.path.join(module_name, filename)
133             fp = tools.file_open(pathname)
134             noupdate = False
135             if kind in ('demo', 'demo_xml'):
136                 noupdate = True
137             try:
138                 if ext == '.csv':
139                     if kind in ('init', 'init_xml'):
140                         noupdate = True
141                     tools.convert_csv_import(cr, module_name, pathname, fp.read(), idref, mode, noupdate)
142                 elif ext == '.sql':
143                     process_sql_file(cr, fp)
144                 elif ext == '.yml':
145                     tools.convert_yaml_import(cr, module_name, fp, idref, mode, noupdate)
146                 else:
147                     tools.convert_xml_import(cr, module_name, fp, idref, mode, noupdate, report)
148             finally:
149                 fp.close()
150
151     if status is None:
152         status = {}
153
154     processed_modules = []
155     statusi = 0
156     pool = pooler.get_pool(cr.dbname)
157     migrations = openerp.modules.migration.MigrationManager(cr, graph)
158     logger.notifyChannel('init', netsvc.LOG_DEBUG, 'loading %d packages..' % len(graph))
159     modobj = pool.get('ir.module.module')
160
161     # register, instantiate and initialize models for each modules
162     for package in graph:
163         module_name = package.name
164         module_id = package.id
165
166         if skip_modules and module_name in skip_modules:
167             continue
168
169         logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading objects' % package.name)
170         migrations.migrate_module(package, 'pre')
171         register_module_classes(package.name)
172         models = pool.instanciate(package.name, cr)
173         if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
174             init_module_models(cr, package.name, models)
175
176         status['progress'] = float(statusi) / len(graph)
177
178         if perform_checks:
179             modobj.check(cr, 1, [module_id])
180
181         idref = {}
182
183         mode = 'update'
184         if hasattr(package, 'init') or package.state == 'to install':
185             mode = 'init'
186
187         if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
188             if package.state=='to upgrade':
189                 # upgrading the module information
190                 modobj.write(cr, 1, [module_id], modobj.get_values_from_terp(package.data))
191             load_init_xml(cr, module_name, idref, mode)
192             load_update_xml(cr, module_name, idref, mode)
193             load_data(cr, module_name, idref, mode)
194             if hasattr(package, 'demo') or (package.dbdemo and package.state != 'installed'):
195                 status['progress'] = (float(statusi)+0.75) / len(graph)
196                 load_demo_xml(cr, module_name, idref, mode)
197                 load_demo(cr, module_name, idref, mode)
198                 cr.execute('update ir_module_module set demo=%s where id=%s', (True, module_id))
199
200                 # launch tests only in demo mode, as most tests will depend
201                 # on demo data. Other tests can be added into the regular
202                 # 'data' section, but should probably not alter the data,
203                 # as there is no rollback.
204                 load_test(cr, module_name, idref, mode)
205
206             processed_modules.append(package.name)
207
208             migrations.migrate_module(package, 'post')
209
210             ver = release.major_version + '.' + package.data['version']
211             # Set new modules and dependencies
212             modobj.write(cr, 1, [module_id], {'state': 'installed', 'latest_version': ver})
213             # Update translations for all installed languages
214             modobj.update_translations(cr, 1, [module_id], None)
215
216             package.state = 'installed'
217             for kind in ('init', 'demo', 'update'):
218                 if hasattr(package, kind):
219                     delattr(package, kind)
220
221         cr.commit()
222         statusi += 1
223
224     cr.commit()
225
226     return processed_modules
227
228 def _check_module_names(cr, module_names):
229     mod_names = set(module_names)
230     if 'base' in mod_names:
231         # ignore dummy 'all' module
232         if 'all' in mod_names:
233             mod_names.remove('all')
234     if mod_names:
235         cr.execute("SELECT count(id) AS count FROM ir_module_module WHERE name in %s", (tuple(mod_names),))
236         if cr.dictfetchone()['count'] != len(mod_names):
237             # find out what module name(s) are incorrect:
238             cr.execute("SELECT name FROM ir_module_module")
239             incorrect_names = mod_names.difference([x['name'] for x in cr.dictfetchall()])
240             logging.getLogger('init').warning('invalid module names, ignored: %s', ", ".join(incorrect_names))
241
242 def load_modules(db, force_demo=False, status=None, update_module=False):
243     # TODO status['progress'] reporting is broken: used twice (and reset each
244     # time to zero) in load_module_graph, not fine-grained enough.
245     # It should be a method exposed by the pool.
246
247     initialize_sys_path()
248
249     open_openerp_namespace()
250
251     force = []
252     if force_demo:
253         force.append('demo')
254
255     cr = db.cursor()
256     try:
257         if not openerp.modules.db.is_initialized(cr):
258             logger.notifyChannel("init", netsvc.LOG_INFO, "init db")
259             openerp.modules.db.initialize(cr)
260             tools.config["init"]["all"] = 1
261             tools.config['update']['all'] = 1
262             if not tools.config['without_demo']:
263                 tools.config["demo"]['all'] = 1
264
265         # This is a brand new pool, just created in pooler.get_db_and_pool()
266         pool = pooler.get_pool(cr.dbname)
267
268         processed_modules = []
269         report = tools.assertion_report()
270         # NOTE: Try to also load the modules that have been marked as uninstallable previously...
271         STATES_TO_LOAD = ['installed', 'to upgrade', 'uninstallable']
272         if 'base' in tools.config['update'] or 'all' in tools.config['update']:
273             cr.execute("update ir_module_module set state=%s where name=%s and state=%s", ('to upgrade', 'base', 'installed'))
274
275         # STEP 1: LOAD BASE (must be done before module dependencies can be computed for later steps) 
276         graph = openerp.modules.graph.Graph()
277         graph.add_module(cr, 'base', force)
278         if not graph:
279             logger.notifyChannel('init', netsvc.LOG_CRITICAL, 'module base cannot be loaded! (hint: verify addons-path)')
280             raise osv.osv.except_osv(_('Could not load base module'), _('module base cannot be loaded! (hint: verify addons-path)'))
281         processed_modules.extend(load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report))
282
283         if tools.config['load_language']:
284             for lang in tools.config['load_language'].split(','):
285                 tools.load_language(cr, lang)
286
287         # STEP 2: Mark other modules to be loaded/updated
288         if update_module:
289             modobj = pool.get('ir.module.module')
290             if ('base' in tools.config['init']) or ('base' in tools.config['update']):
291                 logger.notifyChannel('init', netsvc.LOG_INFO, 'updating modules list')
292                 modobj.update_list(cr, 1)
293
294             _check_module_names(cr, itertools.chain(tools.config['init'].keys(), tools.config['update'].keys()))
295
296             mods = [k for k in tools.config['init'] if tools.config['init'][k]]
297             if mods:
298                 ids = modobj.search(cr, 1, ['&', ('state', '=', 'uninstalled'), ('name', 'in', mods)])
299                 if ids:
300                     modobj.button_install(cr, 1, ids)
301
302             mods = [k for k in tools.config['update'] if tools.config['update'][k]]
303             if mods:
304                 ids = modobj.search(cr, 1, ['&', ('state', '=', 'installed'), ('name', 'in', mods)])
305                 if ids:
306                     modobj.button_upgrade(cr, 1, ids)
307
308             cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base'))
309
310             STATES_TO_LOAD += ['to install']
311
312
313         # STEP 3: Load marked modules (skipping base which was done in STEP 1)
314         loop_guardrail = 0
315         while True:
316             loop_guardrail += 1
317             if loop_guardrail > 100:
318                 raise ValueError('Possible recursive module tree detected, aborting.')
319             cr.execute("SELECT name from ir_module_module WHERE state IN %s" ,(tuple(STATES_TO_LOAD),))
320
321             module_list = [name for (name,) in cr.fetchall() if name not in graph]
322             if not module_list:
323                 break
324
325             new_modules_in_graph = graph.add_modules(cr, module_list, force)
326             if new_modules_in_graph == 0:
327                 # nothing to load
328                 break
329
330             logger.notifyChannel('init', netsvc.LOG_DEBUG, 'Updating graph with %d more modules' % (len(module_list)))
331             processed_modules.extend(load_module_graph(cr, graph, status, report=report, skip_modules=processed_modules))
332
333         # load custom models
334         cr.execute('select model from ir_model where state=%s', ('manual',))
335         for model in cr.dictfetchall():
336             pool.get('ir.model').instanciate(cr, 1, model['model'], {})
337
338         # STEP 4: Finish and cleanup
339         if processed_modules:
340             cr.execute("""select model,name from ir_model where id NOT IN (select distinct model_id from ir_model_access)""")
341             for (model, name) in cr.fetchall():
342                 model_obj = pool.get(model)
343                 if model_obj and not isinstance(model_obj, osv.osv.osv_memory):
344                     logger.notifyChannel('init', netsvc.LOG_WARNING, 'object %s (%s) has no access rules!' % (model, name))
345
346             # Temporary warning while we remove access rights on osv_memory objects, as they have
347             # been replaced by owner-only access rights
348             cr.execute("""select distinct mod.model, mod.name from ir_model_access acc, ir_model mod where acc.model_id = mod.id""")
349             for (model, name) in cr.fetchall():
350                 model_obj = pool.get(model)
351                 if isinstance(model_obj, osv.osv.osv_memory):
352                     logger.notifyChannel('init', netsvc.LOG_WARNING, 'In-memory object %s (%s) should not have explicit access rules!' % (model, name))
353
354             cr.execute("SELECT model from ir_model")
355             for (model,) in cr.fetchall():
356                 obj = pool.get(model)
357                 if obj:
358                     obj._check_removed_columns(cr, log=True)
359                 else:
360                     logger.notifyChannel('init', netsvc.LOG_WARNING, "Model %s is referenced but not present in the orm pool!" % model)
361
362             # Cleanup orphan records
363             pool.get('ir.model.data')._process_end(cr, 1, processed_modules)
364
365         if report.get_report():
366             logger.notifyChannel('init', netsvc.LOG_INFO, report)
367
368         for kind in ('init', 'demo', 'update'):
369             tools.config[kind] = {}
370
371         cr.commit()
372         if update_module:
373             # Remove records referenced from ir_model_data for modules to be
374             # removed (and removed the references from ir_model_data).
375             cr.execute("select id,name from ir_module_module where state=%s", ('to remove',))
376             for mod_id, mod_name in cr.fetchall():
377                 cr.execute('select model,res_id from ir_model_data where noupdate=%s and module=%s order by id desc', (False, mod_name,))
378                 for rmod, rid in cr.fetchall():
379                     uid = 1
380                     rmod_module= pool.get(rmod)
381                     if rmod_module:
382                         # TODO group by module so that we can delete multiple ids in a call
383                         rmod_module.unlink(cr, uid, [rid])
384                     else:
385                         logger.notifyChannel('init', netsvc.LOG_ERROR, 'Could not locate %s to remove res=%d' % (rmod,rid))
386                 cr.execute('delete from ir_model_data where noupdate=%s and module=%s', (False, mod_name,))
387                 cr.commit()
388
389             # Remove menu items that are not referenced by any of other
390             # (child) menu item, ir_values, or ir_model_data.
391             # This code could be a method of ir_ui_menu.
392             # TODO: remove menu without actions of children
393             while True:
394                 cr.execute('''delete from
395                         ir_ui_menu
396                     where
397                         (id not IN (select parent_id from ir_ui_menu where parent_id is not null))
398                     and
399                         (id not IN (select res_id from ir_values where model='ir.ui.menu'))
400                     and
401                         (id not IN (select res_id from ir_model_data where model='ir.ui.menu'))''')
402                 cr.commit()
403                 if not cr.rowcount:
404                     break
405                 else:
406                     logger.notifyChannel('init', netsvc.LOG_INFO, 'removed %d unused menus' % (cr.rowcount,))
407
408             # Pretend that modules to be removed are actually uninstalled.
409             cr.execute("update ir_module_module set state=%s where state=%s", ('uninstalled', 'to remove',))
410             cr.commit()
411     finally:
412         cr.close()
413
414
415 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: