Presentation is loading. Please wait.

Presentation is loading. Please wait.

Introduction to job submission portlets Riccardo Bruno INFN Dpt. Of Web course on the development.

Similar presentations


Presentation on theme: "Introduction to job submission portlets Riccardo Bruno INFN Dpt. Of Web course on the development."— Presentation transcript:

1 Introduction to job submission portlets Riccardo Bruno (riccardo.bruno@ct.infn.it) INFN Dpt. Of Cataniariccardo.bruno@ct.infn.it Web course on the development of applications for Catania Science Gateways, 23 July 2013

2 2 Outline  Pre-requisites  MyJobs portlet Installation  GridOperations table  Portlet Template  Get from SVN  Compile and test  Job submit logging extraction  Code explanations  init() method and preferences  The ACTION/VIEW enums and the portletStatus variable  The AppInput Class  The getInputForm method  The submitJob method  Preferences  Fixing on GridEngine 1.5.1

3 MyJobs (1/2)  MyJob war file available from SourceForge at:  MYJOBSURL=http://sourceforge.net/projects/ctsciencegtwys/files/catania-science- gateway/wars/1307/MyJobs.war/download  curl -L $MYJOBSURL -o MyJobs.war  or, wget $MYJOBSURL -O MyJobs.war  Deploy MyJobs with: cp MyJobs $LIFERAY_HOME/deploy/  Watch the Liferay’ server.log file till:  ‘MyJobs successfully deployed’  Install the portlet in the portal through liferay menu:  Add/More…/INFN/MyJobs

4 MyJobs (2/2) Active Jobs Job Status and Output Search on job description Job description

5 GridOperations table (1/2)  Each user action which involves the distributed infrastructure will be tracked by the UsersTracking Database  The GridEngine uses the GridOperations table to register applications and services accessing the distributed infrastructure mysql -u tracking_user -pusertracking userstracking desc GridOperations; +-------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------------+--------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | portal | varchar(120) | NO | | NULL | | | description | varchar(200) | NO | | NULL | | +-------------+--------------+------+-----+---------+----------------+  We can register and then track the activity of our developed portlet with: insert into GridOperations values (9,'test','testPortlet');

6 GridOperations table (2/2)  How to get GridOperations values  id – Just a numeric value; ‘9’ historically used by Tester Apps  portal – Use the value highlighted in the figure; Liferay’ right, top-most menu:  description – Use any human readable application description  Application registration in the GridOperations table is mandatory for the MyJobs portlet  GridOperations values will be carefully selected for production portals

7 Portlet Template svn checkout http://svn.code.sf.net/p/ctsciencegtwys/liferay/trunk/gilda/mi-hostname-portlet  The portlet template is a complete example of JSR 286 compliant portlet able to submit a sequential Job into a distributed environment.  Its java code extends the GenericPortlet class and uses JSP pages to generate the input GUI  Java code contains java-doc compliant remarks able to generate automatically help pages

8 Portlet Template - Features  It provides a full example on:  Define application default values  Define distributed infrastructure settings where the application will run  Manage input elements from web forms as application input  Manage upload file requests  Manage portlet preferences to handle distributed infrastructure settings  View/Manage the ‘pilot script’ which contains the batch instructions that will be executed on the remote Worker Node (WN).  The portlet template can be ‘customized’ as shown by the previous presentation on the ‘generic’ portlet.  Make a full copy of the mi-hostname-portlet  cp mi-hostname-portlet mi-custom-portlet  Open the customze.sh script inside the mi-custom-portlet directory and change all configurable parameters, then execute it: ./customize.sh  The new portlet is now an hostname clone configured with your settings and ready to be further customized for application specific requirements

9 Portlet Template install and deploy  Compile the template portlet  cd mi-hostname-portlet  ant deploy (then watch the Liferay’ server log file)  Deploy the portlet from Liferay menu as already done for MyJobs  Location: Add/More/GILDA/mi-hostname-portlet  Test the portlet  Press the ‘Demo’ and then ‘Submit’ buttons  It is recommended to watch the Liferay’ server log file during the whole process Bottom Top

10 Portlet Template meaning and usage  mi-hostname-portlet  mi – Multi Infrastructure; it allows to execute the same application on many different insfrastructures (gLite, only)  A multi-Infrastructure+multi-Middleware example exists as well and currently running on Chain project Science Gateway. Its code available on Sourceforge as well.  hostname – The execution of the ‘hostname’ command on the distributed environments is used exactly like the ‘Helloworld’ examples while approaching new programming languages.  It accepts an input file or a text inside the bigger text-area in mutually exclusive fashion. Then just specify a job description string and submit the job with the ‘Submit’ button

11 Before testing the job execution  Open the VPN or be sure the eTokenserver allows incoming connections on port 8082 form your portal IP address  Check the eTokenserver service is reachable  Connect to http://etokenserver.ct.infn.it:8082/eTokenServer/http://etokenserver.ct.infn.it:8082/eTokenServer/  From the interface generate the robot proxy request  Execute curl or wget on the generated request  Check the system date is aligned with current time  On linux check the ntpd service; re-align date as root with: /etc/init.d/ntpd stop ; ntpdate ntp-1.infn.it ; ntpd start  Check that Grid CA certificates are updated  curl http://grid.ct.infn.it/cron_files/grid_settings.tar.gz > grid_settings.tar.gz  tar xvfz grid_settings.tar.gz -C /etc/grid-security/

12 Job Execution Log extraction  Liferay server.log file reports:  A full dump of the portlet preference values  The GridEngine Initialization  The Robot proxy retrieval [mi_hostname_portlet:108] dump: Infrastructure #1 enableInfrastructure : 'yes' nameInfrastructure : 'EUMEDGRID-Support infrastructure' acronymInfrastructure: 'EUMEDGRID' [mi_hostname_portlet:108] dump: Infrastructure #1 enableInfrastructure : 'yes' nameInfrastructure : 'EUMEDGRID-Support infrastructure' acronymInfrastructure: 'EUMEDGRID' INFO JSagaJobSubmission - Getting adaptor name... JSagaJobSubmission - Using adaptor: wms INFO RobotProxy - proxyPath=/tmp/7f7e1e98-0fd1-4ebb-a1ae-0627efddf600 INFO RobotProxy - get proxy: http://etokenserver.ct.infn.it:8082/eTokenServer/eToken/332576f78a4fe70a52048043e90cd11f? voms=gridit:gridit&proxy-renewal=true http://etokenserver.ct.infn.it:8082/eTokenServer/eToken/332576f78a4fe70a52048043e90cd11f? voms=gridit:gridit&proxy-renewal=true INFO RobotProxy - proxyPath=/tmp/7f7e1e98-0fd1-4ebb-a1ae-0627efddf600 INFO RobotProxy - get proxy: http://etokenserver.ct.infn.it:8082/eTokenServer/eToken/332576f78a4fe70a52048043e90cd11f? voms=gridit:gridit&proxy-renewal=true http://etokenserver.ct.infn.it:8082/eTokenServer/eToken/332576f78a4fe70a52048043e90cd11f? voms=gridit:gridit&proxy-renewal=true

13 Job Execution Log extraction  The JSAGA job submission string  The input sandbox file transfers  The job id and the job status thread execution INFO JSagaJobSubmission - jobSandbox:/opt/liferay-portal-6.1.1-ce-ga2/glassfish- 3.1.2/domains/domain1/autodeploy/mi-hostname-portlet/WEB- INF/job/pilot_script.sh>pilot_script.sh,/tmp/20130717133558_test_input_file.txt>201307171335 58_test_input_file.txt,/tmp//jobOutput/multiinfrastructuredemojobdescription1_1/<hostname- Output.txt,/tmp//jobOutput/multiinfrastructuredemojobdescription1_1/<hostname-Error.txt Connecting to Gsiftp service at: wms014.cnaf.infn.it:2811... JSagaJobSubmission - Job Submitted: [wms://wms014.cnaf.infn.it:7443/glite_wms_wmproxy_server]- [https://wms014.cnaf.infn.it:9000/kznSb62LcqCW0fXTiTfr7Q] UsersTrackingDBInterface - UpdateJobsStatusAsync running in Thread : Thread[pool-103-thread- 1,5,grizzly-kernel]

14 The Code mi-hostname-portlet files docroot/ css/ main.css images/ AppLogo.png lib/ submit.jsp viewPilot.jsp edit.jsp help.jsp input.jsp icon.png js/ main.js WEBINF/ (See next slide) build.xml customize.sh css styles definition file Application logo Portlet libraries from third parties JSP pages for: View,Help,Edit portlet modes JSP pages for: View,Help,Edit portlet modes WEBINF Contains portlet configurations, Grid job definition and portlet Java code WEBINF Contains portlet configurations, Grid job definition and portlet Java code Ant build definition file Customization script

15 The Code mi-hostname-portlet files WEB-INF/ classes/ src/it/infn/ct/ AppInfrastructureInfo.java AppLogger.java AppPreferences.java mi_hostname_portlet.java tld/ job/ standard_pilot_script.sh pilot_script.sh liferay-display.xml liferay-portlet.xml glassfish-web.xml portlet.xml css styles definition file Portlet’ Java code Portlet’ XML configuration files XML semantic Grid Job files

16 The Java Code  Source code available form SourceforgeSourceforge  3 Classes:  AppInfrastructureInfo.java  Maintains the information about the distributed infrastructure resources and services  AppLogger.java  Wrapper class dedicated to the logging activity  AppPreferences.java  Contains portlet preferences related to the Grid job and the distributed infrastructure resource settings  mi_hostname_portlet.java  Main portlet code which inherits form the GenericPortlet class. It manages the portlet GUI and instructs the GridEngine to submit the job.

17 mi_hostname_portlet.java  Types and Classes  Actions enumerated types ACTION_INPUT // User asked to submit a job,ACTION_SUBMIT// User asked to rerutn to the input form,ACTION_PILOT // The user did something in the edit pilot screen pane  Views enumerated types VIEW_INPUT // View containing application input fields,VIEW_SUBMIT // View reporting the job submission,VIEW_PILOT // Shows the pilot script and makes it editable  The couple: ( PortletMode, PortletStatus ) determines the entire portlet workflow; both values handled by JSP pages and Java code. The second variable used both for processAction and doView

18 AppInput class  AppInput class defined inside the main Java code source  This class is ment to collect all application GUI inputs and must be dynamically instanciated inside the processAction as soon as the user press the ‘Submit’ button class AppInput { // Applicatoin inputs String inputFileName; // Filename for application input file String inputFileText; // GUI Textfield content String jobIdentifier; // User' given job identifier // Each inputSandobox file must be declared below // This variable contains the content of an uploaded file String inputSandbox_inputFile; // Some user level information // must be stored as well String username; String timestamp; class AppInput { // Applicatoin inputs String inputFileName; // Filename for application input file String inputFileText; // GUI Textfield content String jobIdentifier; // User' given job identifier // Each inputSandobox file must be declared below // This variable contains the content of an uploaded file String inputSandbox_inputFile; // Some user level information // must be stored as well String username; String timestamp;

19 processAction (ActionRequest request, ActionResponse response )  Get portal and user information: portalName, username  From the request object retrieve the PortletMode; an if … else if … chain handles each different mode: EDIT, VIEW, HELP  VIEW  Gets the PortletStatus value (Actions) and use this value inside the switch() statement: ACTION_INPUT, ACTION_SUBMIT, ACTION_PILOT  EDIT  Manages the portlet preferences interface controls  HELP  Does nothing; just Logs the state  ProcessAction sends parameters to the doView() method through the response object

20 doView (RenderRequest request, RenderResponse response )  From request object retrieve the PortletStatus value and determine the variable value currentView and using it for a switch statement  Inside the switch, just select the proper jsp page case VIEW_SUBMIT: { _log.info("VIEW_SUBMIT Selected..."); String jobIdentifier = request.getParameter("jobIdentifier"); request.setAttribute("jobIdentifier", jobIdentifier); PortletRequestDispatcher dispatcher=getPortletContext().\ getRequestDispatcher("/submit.jsp"); dispatcher.include(request, response); } case VIEW_SUBMIT: { _log.info("VIEW_SUBMIT Selected..."); String jobIdentifier = request.getParameter("jobIdentifier"); request.setAttribute("jobIdentifier", jobIdentifier); PortletRequestDispatcher dispatcher=getPortletContext().\ getRequestDispatcher("/submit.jsp"); dispatcher.include(request, response); } // Different actions will be performed accordingly to the // different possible view modes switch(Views.valueOf(currentView)) { … // Different actions will be performed accordingly to the // different possible view modes switch(Views.valueOf(currentView)) { …

21 VIEW Mode Workflow VIEW_INPUT ACTION_SUBMIT->VIEW_SUBMIT ACTION_INPUT->VIEW_INPUT

22 EDIT Mode Workflow doEdit() VIEW_INPUT pref_action : next, previous, add, remove, viewPilot pref_action : next, previous, add, remove, viewPilot VIEW_INPUT VIEW_PILOT

23 Variables: JAVA->JSP  From doView/doEdit/doHelp:  Inside the JSP refer the variable with:  id: Variable name  class: java.lang.String (or any other class)  scope: Normally set to ‘ request ’  Inside JSP code refer the variable value with: request.setAttribute(” ", " "); " class=" " scope=”var scope"/> %>

24 Variables: JSP->Java  Inside the JSP use values inside s :  Inside the Java code refer the variable with (ProcessAction) :  Above example works for ‘normal’ s  mi-hostname-porltet uses a particular kind of  ‘multipart’ allows file upload but requires to manually handle input parameters as done by the getInputForm() method " id=" " … > (String)request.getParameter(" ");

25 getInputForm() (1/2)  Necessary to handle file uploads using the apache’ commons.io.* libraries  Mainly consists of a loop which set the java variables  It uses an enumerated type to identify input controls  Enter the input parameters extraction loop: private enum inputControlsIds { // Input control’ name,… // Other controls }; private enum inputControlsIds { // Input control’ name,… // Other controls }; if (PortletFileUpload.isMultipartContent(request))

26 getInputForm() (2/2)  The loop: FileItemFactory factory = new DiskFileItemFactory(); PortletFileUpload upload = new PortletFileUpload( factory ); List items = upload.parseRequest(request); File repositoryPath = new File("/tmp"); DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory(); diskFileItemFactory.setRepository(repositoryPath); Iterator iter = items.iterator(); while (iter.hasNext()) { FileItem item = (FileItem)iter.next(); String fieldName =item.getFieldName(); String fileName =item.getName(); String contentType=item.getContentType(); boolean isInMemory =item.isInMemory(); long sizeInBytes=item.getSize(); switch(inputControlsIds.valueOf(fieldName)) { case var_name: appInput. =item.getString(); break; FileItemFactory factory = new DiskFileItemFactory(); PortletFileUpload upload = new PortletFileUpload( factory ); List items = upload.parseRequest(request); File repositoryPath = new File("/tmp"); DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory(); diskFileItemFactory.setRepository(repositoryPath); Iterator iter = items.iterator(); while (iter.hasNext()) { FileItem item = (FileItem)iter.next(); String fieldName =item.getFieldName(); String fileName =item.getName(); String contentType=item.getContentType(); boolean isInMemory =item.isInMemory(); long sizeInBytes=item.getSize(); switch(inputControlsIds.valueOf(fieldName)) { case var_name: appInput. =item.getString(); break;

27 processInputFile  Called in case of input files:  processInputFile sets the appInput’ text variable with the uploaded file content (just an example)  mi-hostname-portlet has two utilities:  WARNIG: File content is logged; disable this for binary files processInputFile(FileItem item, AppInput appInput) { File uploadedFile = new File(theNewFileName); try { item.write(uploadedFile); } processInputFile(FileItem item, AppInput appInput) { File uploadedFile = new File(theNewFileName); try { item.write(uploadedFile); } private String updateString(String file) {…} private void storeString(String fileName,String fileContent) {…} private String updateString(String file) {…} private void storeString(String fileName,String fileContent) {…}

28 submitJob (1/4)  Inside the processAction : // Get current preference values getPreferences(request,null); // Create the appInput object AppInput appInput = new AppInput(); // Stores the user submitting the job appInput.username=username; // Determine the submissionTimeStamp SimpleDateFormat dateFormat = new SimpleDateFormat(tsFormat); String timestamp = dateFormat.format(Calendar.getInstance().getTime()); appInput.timestamp=timestamp; // Process input fields and files to upload getInputForm(request,appInput); // Following files have to be updated with // values taken from textareas or from uploaded files: // input_file.txt updateFiles(appInput); // Submit the job submitJob(appInput); // Get current preference values getPreferences(request,null); // Create the appInput object AppInput appInput = new AppInput(); // Stores the user submitting the job appInput.username=username; // Determine the submissionTimeStamp SimpleDateFormat dateFormat = new SimpleDateFormat(tsFormat); String timestamp = dateFormat.format(Calendar.getInstance().getTime()); appInput.timestamp=timestamp; // Process input fields and files to upload getInputForm(request,appInput); // Following files have to be updated with // values taken from textareas or from uploaded files: // input_file.txt updateFiles(appInput); // Submit the job submitJob(appInput);

29 submitJob (2/4) (Old Way)  submitJob() method Instructs the GridEngine to submit  Create the M.I.JobSubmission object:  For development environments:  Setup all enabled infrastructures in the preferences // GridEngine' MultiInfrastructure job submission object MultiInfrastructureJobSubmission miJobSubmission = new MultiInfrastructureJobSubmission(); // GridEngine' MultiInfrastructure job submission object MultiInfrastructureJobSubmission miJobSubmission = new MultiInfrastructureJobSubmission(); miJobSubmission = new MultiInfrastructureJobSubmission( ); // Assigns all enabled infrastructures InfrastructureInfo[] infrastructuresInfo= appPreferences.getEnabledInfrastructures(); for(int i=0; i<infrastructuresInfo.length; i++) miJobSubmission.addInfrastructure(infrastructuresInfo[i]); // Assigns all enabled infrastructures InfrastructureInfo[] infrastructuresInfo= appPreferences.getEnabledInfrastructures(); for(int i=0; i<infrastructuresInfo.length; i++) miJobSubmission.addInfrastructure(infrastructuresInfo[i]);

30 submitJob (3/4) (Old Way)  Describe the job  Submit the job miJobSubmission.setExecutable (…); // Specify the executeable miJobSubmission.setArguments (…); // Specify the application' arguments miJobSubmission.setOutputPath (…); // Specify the output directory miJobSubmission.setOutputFiles(outputSandbox); // Setup output files miJobSubmission.setJobOutput ( outputFile); // std-outputr files miJobSubmission.setJobError ( errorFile); // std-error file miJobSubmission.setInputFiles ( inputSandbox); // input files miJobSubmission.setJDLRequirements(jdlRequirements); miJobSubmission.setExecutable (…); // Specify the executeable miJobSubmission.setArguments (…); // Specify the application' arguments miJobSubmission.setOutputPath (…); // Specify the output directory miJobSubmission.setOutputFiles(outputSandbox); // Setup output files miJobSubmission.setJobOutput ( outputFile); // std-outputr files miJobSubmission.setJobError ( errorFile); // std-error file miJobSubmission.setInputFiles ( inputSandbox); // input files miJobSubmission.setJDLRequirements(jdlRequirements); miJobSubmission.submitJobAsync(appInput.username, portalIPAddress, applicationId, appInput.jobIdentifier);; miJobSubmission.submitJobAsync(appInput.username, portalIPAddress, applicationId, appInput.jobIdentifier);;

31 submitJob (4/4) (New Way, from v1.5.1)  Uses a new object to describe the Job first  Submit the job GEJobDescription description = new GEJobDescription(); description.setExecutable("/bin/sh"); description.setArguments("hostname.sh"); description.setInputFiles(…); description.setOutputPath(….); description.setOutput(…); description.setError(…); miJobSubmission = new MultiInfrastructureJobSubmission(description); // or for development environments // MultiInfrastructureJobSubmission(,description); GEJobDescription description = new GEJobDescription(); description.setExecutable("/bin/sh"); description.setArguments("hostname.sh"); description.setInputFiles(…); description.setOutputPath(….); description.setOutput(…); description.setError(…); miJobSubmission = new MultiInfrastructureJobSubmission(description); // or for development environments // MultiInfrastructureJobSubmission(,description); miJobSubmission.submitJobAsync(appInput.username, portalIPAddress, applicationId, appInput.jobIdentifier);; miJobSubmission.submitJobAsync(appInput.username, portalIPAddress, applicationId, appInput.jobIdentifier);;

32 Preferences (Generic Settings)  Log level  Define different levels for the logging activity: INFO, WARN, ERROR,…  GridOperations id table value  Number of insfrastructures  GridOperations description  Application requirements ‘Job Requirements’  Pilot job, shows the batch that will be executed remotely  UsersTrackingDB connection settings. When !NULL, the portlet will operate in DEBUG mode

33 Preferences (Infrastructure’ Settings)  Enable Infrastructure  TRUE/FALSE flag  Infrastructure number shown  Name of the infrastructure  Infrastructure short name  Information System service  WMS (optional)  GridEngine allow to specify even a list of Computing Elements; this is not handled by the example  RobotProxy settings (Host/Port/VO/Roles/Renewal flag)  LocalProxy (Used to bypass the eTokenserver; using Personal Certificates)  Software Tags specify which resource is capable to satisfy the tag request  WARNING: MultiMiddleware example overrides the meaning of some fields; especially the BDII host which identifies the JSAGA adaptor

34 Preferences (The code)  AppPreferences.java  Class that contains all portlet’ preference values  It provides a method that returns the GridEngine’ InfrastructureInfo object  The portlet Init() method loads default settings from the portlet.xml file (customize.sh creates dynamically this file)  The Init() method creates two object:  AppInitPreferences – Containing default values taken from portlet.xml  AppPreferences – Containing portal administrator’ settings (if changed)  AppInitPreferences – Meant to revert default settings (not implemented)  The customize.sh portlet allow to define default settings for many infrastructures

35 customize.sh  BASH script to clone the mi-hostname-portlet, but renaming the whole project with your specific application details, just changing few environment variables  Essential customization values  AUTH_* values, meant for Application Author details  APP_OPERDESC GridOperation’ description field (optional)  APP_OPERATIONID The GridOperation.Id for this application ( 9 default)  APP_NAME The Application name  APP_VERSION The version number of your application  INIT_NUMINFRASTRUCTURES The number of infrastructures  INIT_n_ Set of infrastructure values  1 <= n <= INIT_NUMINFRASTRUCTURES

36 GridEngine 1.5.1 Fixing (1/2)  Before to proceed with testing, please fix your 1.5.1  Download the new fixed jar (jsaga-job-management-1.5.1_fix.jar)  GEFIXURL=http://sourceforge.net/projects/ctsciencegtwys/files/cat ania-grid-engine/1.5.1_fix/jsaga-job-management- 1.5.1_fix.jar/download  curl -L $GEFIXURL -o /opt/GridEngine/lib/jsaga-job-management- 1.5.1_fix.jar  or, wget $GEFIXURL -O /opt/GridEngine/lib/jsaga-job- management-1.5.1_fix.jar  Download the DB patch SQL file (JobDescription.sql)  GEFIXSQL=http://sourceforge.net/projects/ctsciencegtwys/files/cat ania-grid-engine/1.5.1_fix/JobDescription.sql/download  curl -L $GEFIXSQL -o $HOME/JobDescription.sql  or wget $GEFIXSQL -o $HOME/JobDescription.sql

37 GridEngine 1.5.1 Fixing (2/2)  Now patch your your 1.5.1  Disable the old jar and prepare a symbolic link to the fixed one  mv /opt/GridEngine/lib/jsaga-job-management-1.5.1.jar /opt/GridEngine/lib/jsaga-job-management-1.5.1.jar_disabled  ln -s /opt/GridEngine/lib/jsaga-job-management-1.5.1_fix.jar /opt/GridEngine/lib/jsaga-job-management-1.5.1.jar  Execute the SQL patch  mysql -u tracking_user -pusertracking userstracking < $HOME/JobDescription.sql  Now compile the mi-hostname-portlet and test it  Please report any issue to the list: sg-licence@ct.infn.itsg-licence@ct.infn.it

38 Webinar  Now you are really ready to create your own distributed applications for the Catania Science Gateway!  Next webinar introduces you to more complex jobs; in particular: collections, parametric and workflows

39 More info  GridEngine’ JavadocJavadoc  M.I.JobSubmission ConstructorsConstructors  SVN mi-hostname-portletmi-hostname-portlet  SVN helloworld-portlet (MM/MI) examplehelloworld-portlet

40 40 Thank you !


Download ppt "Introduction to job submission portlets Riccardo Bruno INFN Dpt. Of Web course on the development."

Similar presentations


Ads by Google