Salome HOME
correction bug sat application [27-04-2018-a]
[tools/sat.git] / commands / application.py
1 #!/usr/bin/env python
2 #-*- coding:utf-8 -*-
3 #  Copyright (C) 2010-2013  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 stat
21 import sys
22 import shutil
23 import subprocess
24 import getpass
25
26 from src import ElementTree as etree
27 import src
28
29 parser = src.options.Options()
30 parser.add_option('n', 'name', 'string', 'name',
31     _('Optional: The name of the application (default is APPLICATION.virtual_app.name or '
32       'runAppli)'))
33 parser.add_option('c', 'catalog', 'string', 'catalog',
34     _('Optional: The resources catalog to use'))
35 parser.add_option('t', 'target', 'string', 'target',
36     _('Optional: The directory where to create the application (default is '
37       'APPLICATION.workdir)'))
38 parser.add_option('', 'gencat', 'string', 'gencat',
39     _("Optional: Create a resources catalog for the specified machines "
40       "(separated with ',')\n\tNOTICE: this command will ssh to retrieve "
41       "information to each machine in the list"))
42 parser.add_option('m', 'module', 'list2', 'modules',
43     _("Optional: the restricted list of module(s) to include in the "
44       "application"))
45 parser.add_option('', 'use_mesa', 'boolean', 'use_mesa',
46     _("Optional: Create a launcher that will use mesa products\n\t"
47       "It can be usefull whan salome is used on a remote machine through ssh"))
48
49 ##
50 # Creates an alias for runAppli.
51 def make_alias(appli_path, alias_path, force=False):
52     assert len(alias_path) > 0, "Bad name for alias"
53     if os.path.exists(alias_path) and not force:
54         raise src.SatException(_("Cannot create the alias '%s'\n") % alias_path)
55     else: # find relative path
56         os.symlink(appli_path, alias_path)
57
58 ##
59 # add the definition of a module to out stream.
60 def add_module_to_appli(out, module, has_gui, module_path, logger, flagline):
61     if not os.path.exists(module_path):
62         if not flagline:
63             logger.write("\n", 3, False)
64             flagline = True
65         logger.write("  " + src.printcolors.printcWarning(_(
66                         "WARNING: module %s not installed") % module) + "\n", 3)
67
68     out.write('   <module name="%s" gui="%s" path="%s"/>\n' % (module,
69                                                                has_gui,
70                                                                module_path))
71     return flagline
72
73 ##
74 # Creates the config file to create an application with the list of modules.
75 def create_config_file(config, modules, env_file, logger):
76
77     samples = ""
78     if 'SAMPLES' in config.APPLICATION.products:
79         samples = src.product.get_product_config(config, 'SAMPLES').source_dir
80
81     config_file = src.get_tmp_filename(config, "appli_config.xml")
82     f = open(config_file, "w")
83
84     f.write('<application>\n')
85     if env_file.endswith("cfg"):
86         f.write('<context path="%s"/>\n' % env_file)
87     else:   
88         f.write('<prerequisites path="%s"/>\n' % env_file)
89     f.write('<resources path="CatalogResources.xml"/>\n')
90     f.write('<modules>\n')
91
92     flagline = False
93     for m in modules:
94         mm = src.product.get_product_config(config, m)
95         # do not include in virtual application application module!
96         if src.get_property_in_product_cfg(mm, "is_salome_application") == "yes":
97             continue
98         # do not include products that do not compile
99         if not src.product.product_compiles(mm):
100             continue
101         #obsolete?
102         if src.product.product_is_smesh_plugin(mm):
103             continue
104
105         if 'install_dir' in mm and bool(mm.install_dir):
106             if src.product.product_is_cpp(mm):
107                 # cpp module
108                 for aa in src.product.get_product_components(mm):
109                     install_dir = os.path.join(config.APPLICATION.workdir,
110                                                "INSTALL")
111                     mp = os.path.join(install_dir, aa)
112                     flagline = add_module_to_appli(f,
113                                                    aa,
114                                                    "yes",
115                                                    mp,
116                                                    logger,
117                                                    flagline)
118             else:
119                 # regular module
120                 mp = mm.install_dir
121                 gui = src.get_cfg_param(mm, "has_gui", "yes")
122                 flagline = add_module_to_appli(f, m, gui, mp, logger, flagline)
123
124     f.write('</modules>\n')
125     f.write('<samples path="%s"/>\n' % samples)
126     f.write('</application>\n')
127     f.close()
128
129     return config_file
130
131 ##
132 # Customizes the application by editing SalomeApp.xml.
133 def customize_app(config, appli_dir, logger):
134     if 'configure' not in config.APPLICATION.virtual_app \
135         or len(config.APPLICATION.virtual_app.configure) == 0:
136         return
137
138     # shortcut to get an element (section or parameter) from parent.
139     def get_element(parent, name, strtype):
140         for c in parent.getchildren():
141             if c.attrib['name'] == name:
142                 return c
143
144         # element not found create it
145         elt = add_simple_node(parent, strtype)
146         elt.attrib['name'] = name
147         return elt
148
149     # shortcut method to create a node
150     def add_simple_node(parent, node_name, text=None):
151         n = etree.Element(node_name)
152         if text is not None:
153             try:
154                 n.text = text.strip("\n\t").decode("UTF-8")
155             except:
156                 sys.stderr.write("################ %s %s\n" % (node_name, text))
157                 n.text = "?"
158         parent.append(n)
159         return n
160
161     # read the app file
162     app_file = os.path.join(appli_dir, "SalomeApp.xml")
163     tree = etree.parse(app_file)
164     document = tree.getroot()
165     assert document is not None, "document tag not found"
166
167     logger.write("\n", 4)
168     for section_name in config.APPLICATION.virtual_app.configure:
169         for parameter_name in config.APPLICATION.virtual_app.configure[section_name]:
170             parameter_value = config.APPLICATION.virtual_app.configure[section_name][parameter_name]
171             logger.write("  configure: %s/%s = %s\n" % (section_name,
172                                                         parameter_name,
173                                                         parameter_value), 4)
174             section = get_element(document, section_name, "section")
175             parameter = get_element(section, parameter_name, "parameter")
176             parameter.attrib['value'] = parameter_value
177
178     # write the file
179     f = open(app_file, "w")
180     f.write("<?xml version='1.0' encoding='utf-8'?>\n")
181     f.write(etree.tostring(document, encoding='utf-8'))
182     f.close()
183
184 ##
185 # Generates the application with the config_file.
186 def generate_application(config, appli_dir, config_file, logger):
187     target_dir = os.path.dirname(appli_dir)
188
189     install_KERNEL_dir = src.product.get_product_config(config,
190                                                         'KERNEL').install_dir
191     script = os.path.join(install_KERNEL_dir, "bin", "salome", "appli_gen.py")
192     if not os.path.exists(script):
193         raise src.SatException(_("KERNEL is not installed"))
194     
195     # Add SALOME python in the environment in order to avoid python version 
196     # problems at appli_gen.py call
197     if 'Python' in config.APPLICATION.products:
198         envi = src.environment.SalomeEnviron(config,
199                                              src.environment.Environ(
200                                                               dict(os.environ)),
201                                              True)
202         envi.set_a_product('Python', logger)
203     
204     command = "python %s --prefix=%s --config=%s" % (script,
205                                                      appli_dir,
206                                                      config_file)
207     logger.write("\n>" + command + "\n", 5, False)
208     res = subprocess.call(command,
209                     shell=True,
210                     cwd=target_dir,
211                     env=envi.environ.environ,
212                     stdout=logger.logTxtFile,
213                     stderr=subprocess.STDOUT)
214     
215     if res != 0:
216         raise src.SatException(_("Cannot create application, code = %d\n") % res)
217
218     return res
219
220 ##
221 #
222 def write_step(logger, message, level=3, pad=50):
223     logger.write("%s %s " % (message, '.' * (pad - len(message.decode("UTF-8")))), level)
224     logger.flush()
225
226 ##
227 # Creates a SALOME application.
228 def create_application(config, appli_dir, catalog, logger, display=True):
229       
230     SALOME_modules = get_SALOME_modules(config)
231     
232     warn = ['KERNEL', 'GUI']
233     if display:
234         for w in warn:
235             if w not in SALOME_modules:
236                 msg = _("WARNING: module %s is required to create application\n") % w
237                 logger.write(src.printcolors.printcWarning(msg), 2)
238
239     # generate the launch file
240     retcode = generate_launch_file(config,
241                                    appli_dir,
242                                    catalog,
243                                    logger,
244                                    SALOME_modules)
245     
246     if retcode == 0:
247         cmd = src.printcolors.printcLabel("%s/salome" % appli_dir)
248
249     if display:
250         logger.write("\n", 3, False)
251         logger.write(_("To launch the application, type:\n"), 3, False)
252         logger.write("  %s" % (cmd), 3, False)
253         logger.write("\n", 3, False)
254     return retcode
255
256 def get_SALOME_modules(config):
257     l_modules = []
258     for product in config.APPLICATION.products:
259         product_info = src.product.get_product_config(config, product)
260         if (src.product.product_is_salome(product_info) or 
261                src.product.product_is_generated(product_info)):
262             l_modules.append(product)
263     return l_modules
264
265 ##
266 # Obsolescent way of creating the application.
267 # This method will use appli_gen to create the application directory.
268 def generate_launch_file(config, appli_dir, catalog, logger, l_SALOME_modules):
269     retcode = -1
270
271     if len(catalog) > 0 and not os.path.exists(catalog):
272         raise IOError(_("Catalog not found: %s") % catalog)
273     
274     write_step(logger, _("Creating environment files"))
275     status = src.KO_STATUS
276
277     VersionSalome = src.get_salome_version(config)
278     if VersionSalome>=820:
279         # for salome 8+ we use a salome context file for the virtual app
280         app_shell="cfg"
281         env_ext="cfg"
282     else:
283         app_shell="bash"
284         env_ext="sh"
285
286     try:
287         import environ
288         # generate only shells the user wants (by default bash, csh, batch)
289         # the environ command will only generate file compatible 
290         # with the current system.
291         environ.write_all_source_files(config,
292                                        logger,
293                                        shells=[app_shell],
294                                        silent=True)
295         status = src.OK_STATUS
296     finally:
297         logger.write(src.printcolors.printc(status) + "\n", 2, False)
298
299     # build the application (the name depends upon salome version
300     env_file = os.path.join(config.APPLICATION.workdir, "env_launch." + env_ext)
301
302     write_step(logger, _("Building application"), level=2)
303     cf = create_config_file(config, l_SALOME_modules, env_file, logger)
304
305     # create the application directory
306     os.makedirs(appli_dir)
307
308     # generate the application
309     status = src.KO_STATUS
310     try:
311         retcode = generate_application(config, appli_dir, cf, logger)
312         customize_app(config, appli_dir, logger)
313         status = src.OK_STATUS
314     finally:
315         logger.write(src.printcolors.printc(status) + "\n", 2, False)
316
317     # copy the catalog if one
318     if len(catalog) > 0:
319         shutil.copy(catalog, os.path.join(appli_dir, "CatalogResources.xml"))
320
321     return retcode
322
323
324
325 ##
326 # Generates the catalog from a list of machines.
327 def generate_catalog(machines, config, logger):
328     # remove empty machines
329     machines = map(lambda l: l.strip(), machines)
330     machines = filter(lambda l: len(l) > 0, machines)
331
332     src.printcolors.print_value(logger,
333                                 _("Generate Resources Catalog"),
334                                 ", ".join(machines),
335                                 4)
336     cmd = '"cat /proc/cpuinfo | grep MHz ; cat /proc/meminfo | grep MemTotal"'
337     user = getpass.getuser()
338
339     catfile = src.get_tmp_filename(config, "CatalogResources.xml")
340     catalog = file(catfile, "w")
341     catalog.write("<!DOCTYPE ResourcesCatalog>\n<resources>\n")
342     for k in machines:
343         logger.write("    ssh %s " % (k + " ").ljust(20, '.'), 4)
344         logger.flush()
345
346         ssh_cmd = 'ssh -o "StrictHostKeyChecking no" %s %s' % (k, cmd)
347         p = subprocess.Popen(ssh_cmd, shell=True,
348                 stdin=subprocess.PIPE,
349                 stdout=subprocess.PIPE,
350                 stderr=subprocess.PIPE)
351         p.wait()
352
353         if p.returncode != 0:
354             logger.write(src.printcolors.printc(src.KO_STATUS) + "\n", 4)
355             logger.write("    " + src.printcolors.printcWarning(p.stderr.read()),
356                          2)
357         else:
358             logger.write(src.printcolors.printc(src.OK_STATUS) + "\n", 4)
359             lines = p.stdout.readlines()
360             freq = lines[0][:-1].split(':')[-1].split('.')[0].strip()
361             nb_proc = len(lines) -1
362             memory = lines[-1].split(':')[-1].split()[0].strip()
363             memory = int(memory) / 1000
364
365             catalog.write("    <machine\n")
366             catalog.write("        protocol=\"ssh\"\n")
367             catalog.write("        nbOfNodes=\"1\"\n")
368             catalog.write("        mode=\"interactif\"\n")
369             catalog.write("        OS=\"LINUX\"\n")
370             catalog.write("        CPUFreqMHz=\"%s\"\n" % freq)
371             catalog.write("        nbOfProcPerNode=\"%s\"\n" % nb_proc)
372             catalog.write("        memInMB=\"%s\"\n" % memory)
373             catalog.write("        userName=\"%s\"\n" % user)
374             catalog.write("        name=\"%s\"\n" % k)
375             catalog.write("        hostname=\"%s\"\n" % k)
376             catalog.write("    >\n")
377             catalog.write("    </machine>\n")
378
379     catalog.write("</resources>\n")
380     catalog.close()
381     return catfile
382
383 ##################################################
384
385 ##
386 # Describes the command
387 def description():
388     '''method that is called when salomeTools is called with --help option.
389     
390     :return: The text to display for the application command description.
391     :rtype: str
392     '''
393     return _("The application command creates a SALOME application.\n"
394              "WARNING: it works only for SALOME 6. Use the \"launcher\" "
395              "command for newer versions of SALOME\n\nexample:\nsat application"
396              " SALOME-6.6.0")
397
398 ##
399 # Runs the command.
400 def run(args, runner, logger):
401     '''method that is called when salomeTools is called with application
402        parameter.
403     '''
404     
405     (options, args) = parser.parse_args(args)
406
407     # check for product
408     src.check_config_has_application( runner.cfg )
409
410     application = src.printcolors.printcLabel(runner.cfg.VARS.application)
411     logger.write(_("Building application for %s\n") % application, 1)
412
413     # if section APPLICATION.virtual_app does not exists create one
414     if "virtual_app" not in runner.cfg.APPLICATION:
415         msg = _("The section APPLICATION.virtual_app is not defined in the product.")
416         logger.write(src.printcolors.printcError(msg), 1)
417         logger.write("\n", 1)
418         return 1
419
420     # get application dir
421     target_dir = runner.cfg.APPLICATION.workdir
422     if options.target:
423         target_dir = options.target
424
425     # set list of modules
426     if options.modules:
427         runner.cfg.APPLICATION.virtual_app['modules'] = options.modules
428
429     # activate mesa use in the generated application
430     if options.use_mesa:
431         src.activate_mesa_property(runner.cfg)
432
433     # set name and application_name
434     if options.name:
435         runner.cfg.APPLICATION.virtual_app['name'] = options.name
436         runner.cfg.APPLICATION.virtual_app['application_name'] = options.name + "_appdir"
437     
438     application_name = src.get_cfg_param(runner.cfg.APPLICATION.virtual_app,
439                                          "application_name",
440                                          runner.cfg.APPLICATION.virtual_app.name + "_appdir")
441     appli_dir = os.path.join(target_dir, application_name)
442
443     src.printcolors.print_value(logger,
444                                 _("Application directory"),
445                                 appli_dir,
446                                 3)
447
448     # get catalog
449     catalog, catalog_src = "", ""
450     if options.catalog:
451         # use catalog specified in the command line
452         catalog = options.catalog
453     elif options.gencat:
454         # generate catalog for given list of computers
455         catalog_src = options.gencat
456         catalog = generate_catalog(options.gencat.split(","),
457                                    runner.cfg,logger)
458     elif 'catalog' in runner.cfg.APPLICATION.virtual_app:
459         # use catalog specified in the product
460         if runner.cfg.APPLICATION.virtual_app.catalog.endswith(".xml"):
461             # catalog as a file
462             catalog = runner.cfg.APPLICATION.virtual_app.catalog
463         else:
464             # catalog as a list of computers
465             catalog_src = runner.cfg.APPLICATION.virtual_app.catalog
466             mlist = filter(lambda l: len(l.strip()) > 0,
467                            runner.cfg.APPLICATION.virtual_app.catalog.split(","))
468             if len(mlist) > 0:
469                 catalog = generate_catalog(runner.cfg.APPLICATION.virtual_app.catalog.split(","),
470                                            runner.cfg, logger)
471
472     # display which catalog is used
473     if len(catalog) > 0:
474         catalog = os.path.realpath(catalog)
475         if len(catalog_src) > 0:
476             src.printcolors.print_value(logger,
477                                         _("Resources Catalog"),
478                                         catalog_src,
479                                         3)
480         else:
481             src.printcolors.print_value(logger,
482                                         _("Resources Catalog"),
483                                         catalog,
484                                         3)
485
486     logger.write("\n", 3, False)
487
488     details = []
489
490     # remove previous application
491     if os.path.exists(appli_dir):
492         write_step(logger, _("Removing previous application directory"))
493         rres = src.KO_STATUS
494         try:
495             shutil.rmtree(appli_dir)
496             rres = src.OK_STATUS
497         finally:
498             logger.write(src.printcolors.printc(rres) + "\n", 3, False)
499
500     # generate the application
501     try:
502         try: # try/except/finally not supported in all version of python
503             retcode = create_application(runner.cfg, appli_dir, catalog, logger)
504         except Exception as exc:
505             details.append(str(exc))
506             raise
507     finally:
508         logger.write("\n", 3, False)
509
510     return retcode
511