Salome HOME
add options edit, list, and copy to config command
[tools/sat.git] / src / config.py
1 #!/usr/bin/env python
2 #-*- coding:utf-8 -*-
3 #  Copyright (C) 2010-2012  CEA/DEN
4 #
5 #  This library is free software; you can redistribute it and/or
6 #  modify it under the terms of the GNU Lesser General Public
7 #  License as published by the Free Software Foundation; either
8 #  version 2.1 of the License.
9 #
10 #  This library is distributed in the hope that it will be useful,
11 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 #  Lesser General Public License for more details.
14 #
15 #  You should have received a copy of the GNU Lesser General Public
16 #  License along with this library; if not, write to the Free Software
17 #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
18
19 import os
20 import sys
21 import platform
22 import datetime
23 import glob
24 import re
25 import shutil
26 import gettext
27
28 import common
29
30 # internationalization
31 srcdir = os.path.dirname(os.path.realpath(__file__))
32 gettext.install('salomeTools', os.path.join(srcdir, 'common', 'i18n'))
33
34 # Define all possible option for config command :  sat config <options>
35 parser = common.options.Options()
36 parser.add_option('v', 'value', 'string', 'value', _("print the value of CONFIG_VARIABLE."))
37 parser.add_option('e', 'edit', 'boolean', 'edit', _("edit the product configuration file."))
38 parser.add_option('l', 'list', 'boolean', 'list',_("list all available applications."))
39 parser.add_option('c', 'copy', 'boolean', 'copy',
40     _("""copy a config file to the personnal config files directory.
41 \tWARNING the included files are not copied.
42 \tIf a name is given the new config file takes the given name."""))
43
44 '''
45 class MergeHandler:
46     def __init__(self):
47         pass
48
49     def __call__(self, map1, map2, key):
50         if '__overwrite__' in map2 and key in map2.__overwrite__:
51             return "overwrite"
52         else:
53             return common.config_pyconf.overwriteMergeResolve(map1, map2, key)
54 '''
55
56 class ConfigOpener:
57     def __init__(self, pathList):
58         self.pathList = pathList
59
60     def __call__(self, name):
61         if os.path.isabs(name):
62             return common.config_pyconf.ConfigInputStream(open(name, 'rb'))
63         else:
64             return common.config_pyconf.ConfigInputStream( open(os.path.join( self.getPath(name), name ), 'rb') )
65         raise IOError(_("Configuration file '%s' not found") % name)
66
67     def getPath( self, name ):
68         for path in self.pathList:
69             if os.path.exists(os.path.join(path, name)):
70                 return path
71         raise IOError(_("Configuration file '%s' not found") % name)
72
73 class ConfigManager:
74     '''Class that manages the read of all the configuration files of salomeTools
75     '''
76     def __init__(self, dataDir=None):
77         pass
78
79     def _create_vars(self, application=None, command=None, dataDir=None):
80         '''Create a dictionary that stores all information about machine, user, date, repositories, etc...
81         :param application str: The application for which salomeTools is called.
82         :param command str: The command that is called.
83         :param dataDir str: The repository that contain external data for salomeTools.
84         :return: The dictionary that stores all information.
85         :rtype: dict
86         '''
87         var = {}      
88         var['user'] = common.architecture.get_user()
89         var['salometoolsway'] = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
90         var['srcDir'] = os.path.join(var['salometoolsway'], 'src')
91         var['sep']= os.path.sep
92         
93         # dataDir has a default location
94         var['dataDir'] = os.path.join(var['salometoolsway'], 'data')
95         if dataDir is not None:
96             var['dataDir'] = dataDir
97
98         var['personalDir'] = os.path.join(os.path.expanduser('~'), '.salomeTools')
99
100         # read linux distributions dictionary
101         distrib_cfg = common.config_pyconf.Config(os.path.join(var['dataDir'], "distrib.pyconf"))
102
103         # set platform parameters
104         dist_name = common.architecture.get_distribution(codes=distrib_cfg.DISTRIBUTIONS)
105         dist_version = common.architecture.get_distrib_version(dist_name, codes=distrib_cfg.VERSIONS)
106         dist = dist_name + dist_version
107         
108         # Forcing architecture with env variable ARCH on Windows
109         if common.architecture.is_windows() and "ARCH" in os.environ :
110             bitsdict={"Win32":"32","Win64":"64"}
111             nb_bits = bitsdict[os.environ["ARCH"]]
112         else :
113             nb_bits = common.architecture.get_nb_bit()
114
115         var['dist_name'] = dist_name
116         var['dist_version'] = dist_version
117         var['dist'] = dist
118         var['arch'] = dist + '_' + nb_bits
119         var['bits'] = nb_bits
120         var['python'] = common.architecture.get_python_version()
121
122         var['nb_proc'] = common.architecture.get_nb_proc()
123         node_name = platform.node()
124         var['node'] = node_name
125         var['hostname'] = node_name
126         # particular win case 
127         if common.architecture.is_windows() :
128             var['hostname'] = node_name+'-'+nb_bits
129
130         # set date parameters
131         dt = datetime.datetime.now()
132         var['date'] = dt.strftime('%Y%m%d')
133         var['datehour'] = dt.strftime('%Y%m%d_%H%M%S')
134         var['hour'] = dt.strftime('%H%M%S')
135
136         var['command'] = str(command)
137         var['application'] = str(application)
138
139         # Root dir for temporary files 
140         var['tmp_root'] = os.sep + 'tmp' + os.sep + var['user']
141         # particular win case 
142         if common.architecture.is_windows() : 
143             var['tmp_root'] =  os.path.expanduser('~') + os.sep + 'tmp'
144         
145         return var
146
147     def get_command_line_overrides(self, options, sections):
148         '''get all the overwrites that are in the command line
149         :param options : the options from salomeTools class initialization (like -l5 or --overwrite)
150         :param sections str: The config section to overwrite.
151         :return: The list of all the overwrites to apply.
152         :rtype: list
153         '''
154         # when there are no options or not the overwrite option, return an empty list
155         if options is None or options.overwrite is None:
156             return []
157         
158         over = []
159         for section in sections:
160             # only overwrite the sections that correspond to the option 
161             over.extend(filter(lambda l: l.startswith(section + "."), options.overwrite))
162         return over
163
164     def getConfig(self, application=None, options=None, command=None, dataDir=None):
165         '''get the config from all the configuration files.
166         :param application str: The application for which salomeTools is called.
167         :param options TODO
168         :param command str: The command that is called.
169         :param dataDir str: The repository that contain external data for salomeTools.
170         :return: The final config.
171         :rtype: class 'common.config_pyconf.Config'
172         '''        
173         
174         # create a ConfigMerger to handle merge
175         merger = common.config_pyconf.ConfigMerger()#MergeHandler())
176         
177         # create the configuration instance
178         cfg = common.config_pyconf.Config()
179         
180         # =======================================================================================
181         # create VARS section
182         var = self._create_vars(application=application, command=command, dataDir=dataDir)
183         # add VARS to config
184         cfg.VARS = common.config_pyconf.Mapping(cfg)
185         for variable in var:
186             cfg.VARS[variable] = var[variable]
187
188         for rule in self.get_command_line_overrides(options, ["VARS"]):
189             exec('cfg.' + rule) # this cannot be factorized because of the exec
190         
191         # =======================================================================================
192         # Load INTERNAL config
193         # read src/common/internal_config/salomeTools.pyconf
194         common.config_pyconf.streamOpener = ConfigOpener([os.path.join(cfg.VARS.srcDir, 'common', 'internal_config')])
195         try:
196             internal_cfg = common.config_pyconf.Config(open(os.path.join(cfg.VARS.srcDir, 'common', 'internal_config', 'salomeTools.pyconf')))
197         except common.config_pyconf.ConfigError as e:
198             raise common.SatException(_("Error in configuration file: salomeTools.pyconf\n  %(error)s") % \
199                 {'error': str(e) })
200
201         merger.merge(cfg, internal_cfg)
202
203         for rule in self.get_command_line_overrides(options, ["INTERNAL"]):
204             exec('cfg.' + rule) # this cannot be factorized because of the exec        
205         
206         # =======================================================================================
207         # Load SITE config file
208         # search only in the data directory
209         common.config_pyconf.streamOpener = ConfigOpener([cfg.VARS.dataDir])
210         try:
211             site_cfg = common.config_pyconf.Config(open(os.path.join(cfg.VARS.dataDir, 'site.pyconf')))
212         except common.config_pyconf.ConfigError as e:
213             raise common.SatException(_("Error in configuration file: site.pyconf\n  %(error)s") % \
214                 {'error': str(e) })
215         except IOError as error:
216             e = str(error)
217             if "site.pyconf" in e :
218                 e += "\nYou can copy data" + cfg.VARS.sep + "site.template.pyconf to data" + cfg.VARS.sep + "site.pyconf and edit the file"
219             raise common.SatException( e );
220         
221         # add user local path for configPath
222         site_cfg.SITE.config.configPath.append(os.path.join(cfg.VARS.personalDir, 'Applications'), "User applications path")
223         
224         merger.merge(cfg, site_cfg)
225
226         for rule in self.get_command_line_overrides(options, ["SITE"]):
227             exec('cfg.' + rule) # this cannot be factorized because of the exec
228         
229         # =======================================================================================
230         # Load APPLICATION config file
231         if application is not None:
232             # search APPLICATION file in all directories in configPath
233             cp = cfg.SITE.config.configPath
234             common.config_pyconf.streamOpener = ConfigOpener(cp)
235             try:
236                 application_cfg = common.config_pyconf.Config(application + '.pyconf')
237             except IOError as e:
238                 raise common.SatException(_("%s, use 'config --list' to get the list of available applications.") %e)
239             except common.config_pyconf.ConfigError as e:
240                 raise common.SatException(_("Error in configuration file: %(application)s.pyconf\n  %(error)s") % \
241                     { 'application': application, 'error': str(e) } )
242
243             merger.merge(cfg, application_cfg)
244
245             for rule in self.get_command_line_overrides(options, ["APPLICATION"]):
246                 exec('cfg.' + rule) # this cannot be factorized because of the exec
247         
248         # =======================================================================================
249         # load USER config
250         self.setUserConfigFile(cfg)
251         user_cfg_file = self.getUserConfigFile()
252         user_cfg = common.config_pyconf.Config(open(user_cfg_file))
253         merger.merge(cfg, user_cfg)
254
255         for rule in self.get_command_line_overrides(options, ["USER"]):
256             exec('cfg.' + rule) # this cannot be factorize because of the exec
257
258         return cfg
259
260     def setUserConfigFile(self, config):
261         '''Set the user config file name and path.
262         If necessary, build it from another one or create it from scratch.
263         '''
264         if not config:
265             raise common.SatException(_("Error in setUserConfigFile: config is None"))
266         sat_version = config.INTERNAL.sat_version
267         self.config_file_name = 'salomeTools-%s.pyconf'%sat_version
268         self.user_config_file_path = os.path.join(config.VARS.personalDir, self.config_file_name)
269         if not os.path.isfile(self.user_config_file_path):
270             # if pyconf does not exist, 
271             # Make a copy of an existing  salomeTools-<sat_version>.pyconf
272             # or at least a copy of salomeTools.pyconf
273             # If there is no pyconf file at all, create it from scratch 
274             already_exisiting_pyconf_file = self.getAlreadyExistingUserPyconf( config.VARS.personalDir, sat_version )
275             if already_exisiting_pyconf_file:  
276                 # copy
277                 shutil.copyfile( already_exisiting_pyconf_file, self.user_config_file_path )
278             else: # create from scratch
279                 self.createConfigFile(config)
280     
281     def getAlreadyExistingUserPyconf(self, userDir, sat_version ):
282         '''Get a pyconf file younger than the given sat version in the given directory
283         The file basename can be one of salometools-<younger version>.pyconf or salomeTools.pyconf
284         Returns the file path or None if no file has been found.
285         '''
286         file_path = None  
287         # Get a younger pyconf version   
288         pyconfFiles = glob.glob( os.path.join(userDir, 'salomeTools-*.pyconf') )
289         sExpr = "^salomeTools-(.*)\.pyconf$"
290         oExpr = re.compile(sExpr)
291         younger_version = None
292         for s in pyconfFiles:
293             oSreMatch = oExpr.search( os.path.basename(s) )
294             if oSreMatch:
295                 pyconf_version = oSreMatch.group(1)
296                 if pyconf_version < sat_version: 
297                     younger_version = pyconf_version 
298
299         # Build the pyconf filepath
300         if younger_version :   
301             file_path = os.path.join( userDir, 'salomeTools-%s.pyconf'%younger_version )
302         elif os.path.isfile( os.path.join(userDir, 'salomeTools.pyconf') ):
303             file_path = os.path.join( userDir, 'salomeTools.pyconf' )
304         
305         return file_path 
306     
307     def createConfigFile(self, config):
308         
309         cfg_name = self.getUserConfigFile()
310
311         user_cfg = common.config_pyconf.Config()
312         #
313         user_cfg.addMapping('USER', common.config_pyconf.Mapping(user_cfg), "")
314
315         #
316         user_cfg.USER.addMapping('workDir', os.path.expanduser('~'),
317             "This is where salomeTools will work. You may (and probably do) change it.\n")
318         user_cfg.USER.addMapping('cvs_user', config.VARS.user,
319             "This is the user name used to access salome cvs base.\n")
320         user_cfg.USER.addMapping('svn_user', config.VARS.user,
321             "This is the user name used to access salome svn base.\n")
322         user_cfg.USER.addMapping('output_level', 3,
323             "This is the default output_level you want. 0=>no output, 5=>debug.\n")
324         user_cfg.USER.addMapping('publish_dir', os.path.join(os.path.expanduser('~'), 'websupport', 'satreport'), "")
325         user_cfg.USER.addMapping('editor', 'vi', "This is the editor used to modify configuration files\n")
326         user_cfg.USER.addMapping('browser', 'firefox', "This is the browser used to read html documentation\n")
327         user_cfg.USER.addMapping('pdf_viewer', 'evince', "This is the pdf_viewer used to read pdf documentation\n")
328         # 
329         common.ensure_path_exists(config.VARS.personalDir)
330         common.ensure_path_exists(os.path.join(config.VARS.personalDir, 'Applications'))
331
332         f = open(cfg_name, 'w')
333         user_cfg.__save__(f)
334         f.close()
335         print(_("You can edit it to configure salomeTools (use: sat config --edit).\n"))
336
337         return user_cfg   
338
339     def getUserConfigFile(self):
340         '''Get the user config file
341         '''
342         if not self.user_config_file_path:
343             raise common.SatException(_("Error in getUserConfigFile: missing user config file path"))
344         return self.user_config_file_path     
345         
346     
347 def print_value(config, path, show_label, level=0, show_full_path=False):
348     '''Prints a value from the configuration. Prints recursively the values under the initial path.
349     :param config class 'common.config_pyconf.Config': The configuration from which the value is displayed.
350     :param path str : the path in the configuration of the value to print.
351     :param show_label boolean: if True, do a basic display. (useful for bash completion)
352     :param level int: The number of spaces to add before display.
353     :param show_full_path :
354     :return: The final config.
355     :rtype: class 'common.config_pyconf.Config'
356     '''            
357     
358     # display all the path or not
359     if show_full_path:
360         vname = path
361     else:
362         vname = path.split('.')[-1]
363
364     # number of spaces before the display
365     tab_level = "  " * level
366     
367     # call to the function that gets the value of the path.
368     try:
369         val = config.getByPath(path)
370     except Exception as e:
371         sys.stdout.write(tab_level)
372         sys.stdout.write("%s: ERROR %s\n" % (common.printcolors.printcLabel(vname), common.printcolors.printcError(str(e))))
373         return
374
375     # in this case, display only the value
376     if show_label:
377         sys.stdout.write(tab_level)
378         sys.stdout.write("%s: " % common.printcolors.printcLabel(vname))
379
380     # The case where the value has under values, do a recursive call to the function
381     if dir(val).__contains__('keys'):
382         if show_label: sys.stdout.write("\n")
383         for v in sorted(val.keys()):
384             print_value(config, path + '.' + v, show_label, level + 1)
385     elif val.__class__ == common.config_pyconf.Sequence or isinstance(val, list): # in this case, value is a list (or a Sequence)
386         if show_label: sys.stdout.write("\n")
387         index = 0
388         for v in val:
389             print_value(config, path + "[" + str(index) + "]", show_label, level + 1)
390             index = index + 1
391     else: # case where val is just a str
392         sys.stdout.write("%s\n" % val)
393
394 def description():
395     return _("The config command allows manipulation and operation on config files.")
396     
397
398 def run(args, runner):
399     (options, args) = parser.parse_args(args)
400     
401     # case : print a value of the config
402     if options.value:
403         if options.value == ".":
404             # if argument is ".", print all the config
405             for val in sorted(runner.cfg.keys()):
406                 print_value(runner.cfg, val, True)
407             return
408         print_value(runner.cfg, options.value, True, level=0, show_full_path=False)
409         return
410     
411     # case : edit user pyconf file or application file
412     elif options.edit:
413         editor = runner.cfg.USER.editor
414         if 'APPLICATION' not in runner.cfg: # edit user pyconf
415             usercfg = os.path.join(runner.cfg.VARS.personalDir, 'salomeTools-%s.pyconf'%runner.cfg.INTERNAL['sat_version'])
416             common.fileSystem.show_in_editor(editor, usercfg)
417         else:
418             # search for file <application>.pyconf and open it
419             for path in runner.cfg.SITE.config.configPath:
420                 pyconf_path = os.path.join(path, runner.cfg.VARS.application + ".pyconf")
421                 if os.path.exists(pyconf_path):
422                     common.fileSystem.show_in_editor(editor, pyconf_path)
423                     break
424     
425     # case : copy an existing <application>.pyconf to ~/.salomeTools/Applications/LOCAL_<application>.pyconf
426     elif options.copy:
427         # product is required
428         common.check_config_has_application( runner.cfg )
429
430         # get application file path 
431         source = runner.cfg.VARS.application + '.pyconf'
432         source_full_path = ""
433         for path in runner.cfg.SITE.config.configPath:
434             # ignore personal directory
435             if path == runner.cfg.VARS.personalDir:
436                 continue
437             # loop on all directories that can have pyconf applications
438             zz = os.path.join(path, source)
439             if os.path.exists(zz):
440                 source_full_path = zz
441                 break
442
443         if len(source_full_path) == 0:
444             raise common.SatException(_("Config file for product %s not found\n") % source)
445         else:
446             if len(args) > 0:
447                 # a name is given as parameter, use it
448                 dest = args[0]
449             elif 'copy_prefix' in runner.cfg.SITE.config:
450                 # use prefix
451                 dest = runner.cfg.SITE.config.copy_prefix + runner.cfg.VARS.application
452             else:
453                 # use same name as source
454                 dest = runner.cfg.VARS.application
455                 
456             # the full path
457             dest_file = os.path.join(runner.cfg.VARS.personalDir, 'Applications', dest + '.pyconf')
458             if os.path.exists(dest_file):
459                 raise common.SatException(_("A personal application '%s' already exists") % dest)
460             
461             # perform the copy
462             shutil.copyfile(source_full_path, dest_file)
463             print(_("%s has been created.") % dest_file)
464     
465     # case : display all the available pyconf applications
466     elif options.list:
467         lproduct = list()
468         # search in all directories that can have pyconf applications
469         for path in runner.cfg.SITE.config.configPath:
470             # print a header
471             sys.stdout.write("------ %s\n" % common.printcolors.printcHeader(path))
472
473             if not os.path.exists(path):
474                 sys.stdout.write(common.printcolors.printcError(_("Directory not found")) + "\n")
475             else:
476                 for f in sorted(os.listdir(path)):
477                     # ignore file that does not ends with .pyconf
478                     if not f.endswith('.pyconf'):
479                         continue
480
481                     appliname = f[:-len('.pyconf')]
482                     if appliname not in lproduct:
483                         lproduct.append(appliname)
484                         if path.startswith(runner.cfg.VARS.personalDir):
485                             sys.stdout.write("%s*\n" % appliname)
486                         else:
487                             sys.stdout.write("%s\n" % appliname)
488
489             sys.stdout.write("\n")
490     
491