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