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