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