, , Récupérer les logs d’une commande shell QSH

Interrogation SQL des tables système


Le passage de commandes shell est tout à fait possible dans les programmes de CL (langage de contrôle sur IBM i) via QSH CMD(&maCommande). Si dans une session QSH (STRQSH) l’état des logs s’affiche, nous n’avons pas ces retours quand les commandes shell sont passées via un programme CL.

plus d’informations sur le shell QSH sous IBM i : LE SHELL INTERPRETER

Contexte

Les deux écrans suivants permettent de tester l’authentification à Github (plateforme de versionnage du code, de contrôle et de collaboration ) ; ce travail servira d’exemple à notre article :

  • Commandes manuelles, dans une session QSH avec les logs (en rouge)
  • Commande automatisée dans un programme CL, sans retour de logs

Il existe tout de même un moyen pour récupérer les logs d’une commande shell passée via un programme CL ; l’interrogation des tables système par requête SQL.

Principe

Par un programme CL, les commandes QSH sont soumises dans un travail via SBMJOB

Plusieurs paramètres sont nécessaires :

  • la commande QSH (ici &QSH)

  • le travail doit être identifiable par un nom &JOBNAME, une bibliotheque &LIB, une file de travail &JOBQ, une file de sortie &OUTQ et un utilisateur &USER ; que l’on peut déclarer de cette manière

les noms de variables et leurs valeurs sont arbitraires

  • l’instant t de l’exécution &TIMESTAMP

Ce sont ces paramètres qui alimentent notre requête SQL, et nous permettent de trouver les logs.

VUE qsys2.output_queue_entries

La première composante de notre requête SQL est la récupération des infos du travail en fonction des paramètres, expliqués ci-dessus, via la vue système qsys2.output_queue_entries.

Encapsulation et récupération de la liste des logs d’un travail

Nous récupérons la dernière ligne entrée dans qsys2.output_queue_entries (ORDER BY create_timestamp DESC FETCH FIRST ROW ONLY) que nous encapsulons dans un WITH ; ce qui correspond aux identifiants associés au dernier travail lancé par le programme CL.

Le résultat du WITH (alias lastLog ici) est passé dans un SELECT sur la vue systools.spooled_file_data pour récupérer la liste des spools associés à la commande QSH (variable &QSH dans notre exemple): WHERE spooled_data like trim(:log) || '%'.

Nous obtenons les spools associées à une (ou plusieurs) commandes QSH lancé(es) via un programme CL

Intégration dans un programme

A ce niveau, nous sommes capable d’obtenir les logs générées par une commande QSH pour consultation ; en passant nos requêtes SQL dans un exécuteur de script. Voyons maintenant comment il est possible d’obtenir ces logs par un programme et d’adapter le traitement en fonction de leurs valeurs.

Programme CL

L’idée est d’avoir notre programme CL (ici ggitAuth.clle) qui :

  • soumet le travail,

  • appelle un programme SQLRPGLE qui retourne la log (ici getqshlog.sqlrpgle),

  • effectue le traitement en fonction de la valeur de la log retournée par le programme SQLRPGLE.

Programme SQLRPGLE

Le programme SQLRPGLE reçoit en paramètre :

  • la valeur de la log ciblé log,
  • la file de sortie OUTQ,
  • la file de travail JOBQ,
  • le nom du travail JOBNAME,
  • le temps d’exécution TIMESTAMP.

Par un exec sql, nous retrouvons l’interrogation de table vue précédemment. Celle-ci est intégrée dans une boucle pour gérer le délai d’écriture dans la table au moment du passage de la commande QSH :

Enfin nous mettons dans une variable la valeur de la log ; qui prend ‘Log not found’ en cas d’échec de la requête (sqlCode <> 0).


En résumé

Il est possible dans un environnement IBM i d’exécuter des commandes shell comparables à ce qui peut se faire sur UNIX. Les logs générées par ces commandes sont consultables par interrogation de tables SQL. Pour aller plus loin, la récupération des logs pour analyse dans un programme permet la prise de décision dans ce dernier.

, , Afficher une fenêtre

Comment Afficher une fenêtre dynamique ?

Vous avez besoin d’afficher une fenêtre avec un titre est un texte , par exemple dans des opérations d’administration

Vous pouvez utiliser un écran de type DSPF et un programme associé voici une alternative intéressante en utilisant DSM (Dynamic Screen Manager) qui vous permettra de créer dynamiquement un écran à la volée sans source à compiler

Le source est disponible ici

https://github.com/Plberthoin/PLB/tree/master/WINDOW

Exemple :

avec un texte et un titre :

Pour en savoir plus sur DSM

https://www.ibm.com/docs/en/i/7.3?topic=ssw_ibm_i_73/apis/dsm.html

Avec DSM vous pouvez gérer entièrement votre affichage …

, , , Utilisation d’invite sélective

Vous connaissez et utilisez l’API QCMDEXC qui permet d’exécuter un commande IBMI

Exemple

CALL QCMDEXC (‘ADDLIBLE STITE’ 25)

Vous pouvez utilisez aussi par SQL sous forme de fonction ou procédure

Et enfin elle sera utilisée si vous faites un menu par SDA ou par UIM

Pour avoir une invite sélective vous devrez indiquer un ? avant la commande

Exemple


?WRKSPLF

Pour exécuter la commande WRKSPLF en ayant un invite équivalent à si vous aviez utilisé <F4>

Mais vous pouvez vouloir que certain paramètres soient affichables sans saisie d’autre pas affichable etc …

On va pouvoir indiquer devant les paramètres de la commande 2 caractères, pour indiquer une option de traitement de l’invite

?? le paramètre est modifiable et visible
?* le paramètre est vue mais pas modifiable
?– le paramètre n’est pas affichée

Ci dessous un tableau récapitulatif

Exemple

Sur la commande de gestion des fonctions usage , dans une procédure SQL

Ce qui produira un résultat suivant ou les 2 paramètres DEFAULT et ALLOBJAUT ne seront pas affichés

Le lien à connaitre pour les informations complètes

https://www.ibm.com/docs/en/i/7.3?topic=time-using-selective-prompting-cl-commands

, , , Informations sur les commandes

Avec la version TR1 de 7.5 et la TR7 de la 7.4

IBM met à dispo un service (COMMAND_INFO) qui va permettre d’avoir des informations sur les commandes de votre partition IBM i.

Voici 2 idées d’utilisation de cette vue QSYS2.COMMAND_INFO

Par exemple, pour connaitre les commandes qui sont permises en environnement restreint

SELECT *
FROM QSYS2.COMMAND_INFO
WHERE ALLOW_LIMITED_USER = ‘*YES’

Par exemple, pour savoir si un programme est utilisé dans une commande lorsqu’on fait une analyse d’impact avant une modification

SELECT COMMAND_NAME,
COMMAND_PROCESSING_PROGRAM_LIBRARY,
COMMAND_PROCESSING_PROGRAM
FROM QSYS2.COMMAND_INFO
WHERE COMMAND_LIBRARY = ‘GFTP’

Conclusion :

Cette vue fournit des informations intéressantes et faciles à croiser avec d’autres éléments sur toutes les commandes présentes sur vos systèmes.

Lorsque l’on crée un programme de service il est intéressant de classer les procédures utilisées dans différents modules afin de faciliter une maintenance future. On peut regrouper par exemple les procédures par fonctionnalité métier (prise de commandes, rangement, calculs de taux,… ) ou par proximité technique (manipulation de chaines de caractères, calcul d’un modulo, manipulation de dates,… ). Dans l’article intitulé « CONTROLER IBAN & RIB » on regroupe les deux fonctions bancaires au sein d’une même procédure.

Lorsque l’on veut créer un programme de service il faut procéder en deux temps :

  • Créer les modules contenant les procédures et fonctions
CRTSQLRPGI OBJ(MyLibrary/MyModuleXX) SRCFILE(MyLibrary/MySRCPF) SRCMBR(MyModuleXX) OBJTYPE(MODULE) REPLACE(YES) DBGVIEW(*SOURCE)
  • Créer le programme de service qui intègrera les modules
CRTSRVPGM SRVPGM(MyLibrary/MyServicePGM) MODULE(MyLibrary/MyModule0 MyLibrary/MyModule1 MyLibrary/MyModule2 MyLibrary/MyModule3 MyLibrary/MyModule4) EXPORT(*SRCFILE) SRCFILE(MyLibrary/QSRVSRC) SRCMBR(MySrcMBR)

Nous vous proposons donc de créer une fonction qui permettra d’enchainer ces deux opérations (nous nous limiterons à la possibilité d’agréger 10 modules dans un programme de service)

Présentation de la commande

Cette commande permet de saisir plusieurs modules et de les intégrer directement dans un programme de service

On peut également ajouter un répertoire de liage, si besoin

Source de la commande

             CMD        PROMPT('Création Programme de Service')
             PARM       KWD(SRVPGM) TYPE(Q0) MIN(1) PROMPT('Programme de service ')
 Q0:         QUAL       TYPE(*NAME) LEN(10) MIN(1)
             QUAL       TYPE(*NAME) LEN(10) DFT(*CURLIB) SPCVAL((*CURLIB)) MIN(0) +
                          PROMPT('Bibliothèque           ')
             PARM       KWD(SRVSRC) TYPE(Q1) PROMPT('Fichier source d''exportation')
 Q1:         QUAL       TYPE(*NAME) LEN(10) DFT(QSRVSRC) MIN(0)
             QUAL       TYPE(*NAME) LEN(10) DFT(*CURLIB) SPCVAL((*CURLIB)) MIN(0) +
                          PROMPT('Bibliothèque           ')
             QUAL       TYPE(*NAME) LEN(10) DFT(*SRVPGM) SPCVAL((*SRVPGM)) MIN(0) +
                          PROMPT('Membre source d''exportation')
             PARM       KWD(MODULE) TYPE(*NAME) LEN(10) MAX(10) PROMPT('Modules du Programme ')
             PARM       KWD(SRCFIL) TYPE(*NAME) LEN(10) MIN(0) DFT(QRPGLESRC) PROMPT('Fichier +
                          source Programme ')
             PARM       KWD(ONBNDDIR) TYPE(*CHAR) LEN(1) MIN(0) PROMPT('Répertoire de Liage O/N  ')
             PARM       KWD(BNDDIR) TYPE(*NAME) LEN(10) MIN(0) PMTCTL(LIAGE) +
                          PROMPT('Répertoire de Liage  ')

 LIAGE:      PMTCTL     CTL(SRCFIL) COND((*NE '')) NBRTRUE(*ALL)
             PMTCTL     CTL(SRVSRC) COND((*NE '')) NBRTRUE(*ALL) LGLREL(*AND)
             PMTCTL     CTL(ONBNDDIR) COND((*EQ 'O')) NBRTRUE(*ALL) LGLREL(*AND)
 LIAG2:      PMTCTL     CTL(ONBNDDIR) COND((*EQ 'O') (*EQ 'N')) NBRTRUE(*EQ 1)           

Programme de controle

PGM        PARM(&SRVPGMLIB &SRVBIBMBR &MODULE &SRCFIL +
                          &ONBNDDIR &BNDDIR)

             DCL        VAR(&VAR1) TYPE(*INT) LEN(2)
             DCL        VAR(&POS) TYPE(*INT) LEN(2)
             DCL        VAR(&CPT) TYPE(*INT) LEN(2)
             DCL        VAR(&SRVPGMLIB) TYPE(*CHAR) LEN(20)
             DCL        VAR(&SRVPGM) TYPE(*CHAR) STG(*DEFINED) LEN(10) +
                          DEFVAR(&SRVPGMLIB 1)
             DCL        VAR(&SRVLIB) TYPE(*CHAR) STG(*DEFINED) LEN(10) +
                          DEFVAR(&SRVPGMLIB 11)
             DCL        VAR(&SRVBIBMBR) TYPE(*CHAR) LEN(30)
             DCL        VAR(&SRVFIC) TYPE(*CHAR) STG(*DEFINED) LEN(10) +
                          DEFVAR(&SRVBIBMBR 1)
             DCL        VAR(&SRVBIB) TYPE(*CHAR) STG(*DEFINED) LEN(10) +
                          DEFVAR(&SRVBIBMBR 11)
             DCL        VAR(&SRVMBR) TYPE(*CHAR) STG(*DEFINED) LEN(10) +
                          DEFVAR(&SRVBIBMBR 21)
             DCL        VAR(&MODULE) TYPE(*CHAR) LEN(102)
             DCL        VAR(&VALMOD) TYPE(*CHAR) LEN(10)
             DCL        VAR(&SRCFIL) TYPE(*CHAR) LEN(10)
             DCL        VAR(&ONBNDDIR) TYPE(*CHAR) LEN(1)
             DCL        VAR(&BNDDIR) TYPE(*CHAR) LEN(10)
             DCL        VAR(&LSTMOD) TYPE(*CHAR) LEN(250)
             DCL        VAR(&COMMANDE) TYPE(*CHAR) LEN(500)

             /* Initialiser les variables */
             CHGVAR     VAR(&VAR1) VALUE(%BIN(&MODULE 1 2))
             IF         COND(&SRVLIB *EQ '*CURLIB') THEN(RTVJOBA +
                          CURLIB(&SRVLIB))


             /* Extraction de chaque poste */
             DOFOR      VAR(&CPT) FROM(1) TO(&VAR1)
                CHGVAR     VAR(&POS) VALUE(3 + (&CPT - 1) * 10)
                CHGVAR     VAR(&VALMOD) VALUE(%SST(&MODULE &POS 10))

                CRTSQLRPGI OBJ(&SRVLIB/&VALMOD) SRCFILE(&SRVLIB/&SRCFIL) +
                             SRCMBR(&VALMOD) OBJTYPE(*MODULE) +
                             REPLACE(*YES) DBGVIEW(*SOURCE)

                CHGVAR     VAR(&LSTMOD) VALUE(%TRIM(&LSTMOD) !! ' ' !! +
                             %TRIM(&SRVLIB) !! '/' !! %TRIM(&VALMOD))

             ENDDO

             CHGVAR     VAR(&COMMANDE) VALUE('CRTSRVPGM  SRVPGM(' !! +
                          %TRIM(&SRVLIB) !! '/' !! %TRIM(&SRVPGM) !! ') +
                          MODULE(' !! %TRIM(&LSTMOD) !! ') +
                          EXPORT(*SRCFILE) SRCFILE(' !! %TRIM(&SRVBIB) !! +
                          '/' !! %TRIM(&SRVFIC) !! ') SRCMBR(' !! +
                          %TRIM(&SRVMBR) !! ')')

             CALL       PGM(QCMDEXC) PARM(&COMMANDE  %LEN(&COMMANDE))

             IF         COND(&ONBNDDIR = 'O') THEN(DO)
                CHKOBJ     OBJ(&SRVLIB/&BNDDIR) OBJTYPE(*BNDDIR)
                MONMSG     MSGID(CPF9801) EXEC(CRTBNDDIR  +
                             BNDDIR(&SRVLIB/&BNDDIR))
                ADDBNDDIRE BNDDIR(&SRVLIB/&BNDDIR) OBJ((&SRVLIB/&SRVPGM))
             ENDDO

             ENDPGM                                 

Vous connaissez tous les notions de cette bibliothèque :

Elle n’est pas sauvegardée à la fin d’un travail et chaque travail a sa QTEMP.

Elle est généralement dans votre liste de bibliothèque.

Maintenant imaginez que vous utilisez un logiciel et que vous voulez être sûr que QTEMP soit la première bibliothèque de votre liste :

Vous pouvez l’enlever et la remettre en entête mais attention, l’enlever revient à la supprimer. Si vous avez des données par exemple des fichiers à l’intérieur, ils seront donc perdus, je propose donc un petit programme qui pourra réaliser ça pour vous :

PGM                                                                   
/* MISE EN PLACE QTEMP EN TETE DE LISTE */                            
dcl &time *char 6                                                     
             DCL        VAR(&NBR) TYPE(*DEC) LEN(10)                  
             dcl &libl *char 2750                                     
             dcl  &req *char 512                                      
 /* Recherche si QTEMP est entete on ne fait rien                  */ 
             RTVJOBA    USRLIBL(&LIBL)                                
             IF COND(%sst(&libl 1 10) = 'QTEMP') THEN(DO)             
             goto fin                                                 
             ENDDO                                                    
 /* Recherche si QTEMP est pas en tete de liste                    */ 
             ELSE                                                     
             DSPOBJD    OBJ(QTEMP/*ALL) OBJTYPE(*ALL) +               
                          OUTPUT(*OUTFILE) OUTFILE(QTEMP/WRESULT)     
             MONMSG     MSGID(CPF2123) exec(do)                       
 /* si bibliothèque vide */                                           
             RMVLIBLE   LIB(QTEMP)                                    
             ADDLIBLE   LIB(QTEMP) POSITION(*FIRST)                   
             GOTO       CMDLBL(FIN)                                 
             ENDDO                                                  
 /* Si bibliothèque QTEMP remplie */                                 
             CRTLIB     LIB('W' *TCAT &TIME) TYPE(*TEST) TEXT('Bib +
                          sauvegarde qtemp')                        
             monmsg CPF2111 exec(do)                                
             CLRLIB     LIB('W' *TCAT &TIME)                        
             ENDDO                                                  
             CRTDUPOBJ  OBJ(*ALL) FROMLIB(QTEMP) OBJTYPE(*FILE) +   
                          TOLIB('W' *TCAT &TIME) DATA(*YES)         
   monmsg    CPF2130                                                
             rmvlible qtemp                                         
             addlible qtemp  *first                                 
             CRTDUPOBJ  OBJ(*ALL) FROMLIB('W' *TCAT &TIME) +        
                          OBJTYPE(*FILE) TOLIB(QTEMP) DATA(*YES)    
   monmsg    CPF2130                                                
             DLTLIB     LIB('W' *TCAT &TIME)                        
 /* fin du programme */                                             
 fin:
            SNDPGMMSG  MSGID(CPF9898) MSGF(QCPFMSG) +               
                         MSGDTA('Bibliothèque Qtemp placée en tête +
                         de liste') MSGTYPE(*COMP)                  
            ENDPGM                                                                                                                

Voila, vous avez une exemple tout en CLP , qui permet de mettre QTEMP en tête de liste sans perdre les informations à l’intérieur.

Merci à Jean-Marie pour son aide.

, , Appel d’API en CL

Nous utilisons de plus en plus les services SQL pour de nombreuses actions : accès aux *DTAQ, *DTAARA, travaux etc …

Par exemple, pour accéder au contenu d’une variable d’environnement, la vue QSYS2.ENVIRONMENT_VARIABLE_INFO est très simple à utiliser :

Dans un programme RPG, il est très simple d’effectuer une lecture SQL.

Mais dans certains cas, nous préférerons utiliser les APIs système : pour la performance, ou dans le CL ! En CL, il est possible d’exécuter une instruction SQL, mais pas de récupérer un result sets (en tout cas pas simplement et avec une bonne performance).

L’appel d’API depuis le CL ne pose pas de soucis, principalement depuis les évolutions permettant de maitriser l’allocation mémoire des variables (variables autonomes, basées sur un pointeur, ou basée sur une autre variable – équivalent OVERLAY du RPG) :

Maintenant, lorsque l’on utilise des API, il est (souvent) nécessaire de passer par des pointeurs, de façon explicite ou non. Par exemple, c’est le cas pour QUSRJOBI même si l’on a pas déclaré de pointeur dans notre programme CL :

En réalité, lors d’un appel de programme ou procédure, les paramètres sont transmis par défaut par référence : concrètement on passe un pointeur sur le début de la valeur, mais pas la valeur elle-même !

Il faut commencer à interpréter les prototypes des API en fonction du langage d’appel …

Le système étant écrit en C, certaines API utilisent la définition C d’une chaine de caractères : un pointeur sur le premier caractère, la fin de la chaîne étant marquée par le premier octet null !

En RPG, nous avons la fonction %str() qui permet de gérer ceci :

En CL, pas de fonction équivalent pour gérer cela.

Prenons par exemple l’API getEnv() (en fait une fonction de la bibliothèque standard C) :

Il faut fournir en entrée le nom de la variable d’environnement, au format C String, c’est à dire à terminaison nulle.

De même, la valeur de retour est un pointeur sur le début de la valeur. La fin se situe au premier octet null rencontré …

Voici un exemple d’appel :

Nous déclarons simplement une variable de type CHAR(1) initialisée à x’00’ :

DCL VAR(&SPC_INIT) TYPE(*CHAR) LEN(1) VALUE(X'00')

Cela nous sert à créer la valeur pour l’appel : chaine à terminaison nulle contenant le nom de la variable d’environnement recherchée (ici CLASSPATH) :

CHGVAR &ENVVAR ( 'CLASSPATH' *CAT &SPC_INIT )

Pour décoder la valeur de retour, il nous faut parcourir la valeur reçue jusqu’à trouver la terminaison nulle. On utilise simplement %SCAN pour trouver la position et redécouper :

CHGVAR VAR(&POSNULL) VALUE(%SCAN(&SPC_INIT &ENVVARVAL) - 1 )
CHGVAR &FINALVAL %SUBSTRING(&ENVVARVAL 1 &POSNULL)

Au final, très peu de code à ajouter, mais nécessite une compréhension de la documentation des APIs, des types de données dans les différents langages, et des mécanismes de transmission de paramètres !


, , , Tuez les jobs SSH dans QINTER

Si vous commencez à faire du SSH (en tant que client), par exemple pour faire du GitHub, vous avez des jobs qui vont rester et que vous devrez tuer par un kill ou un ENDJOB.

C’est des jobs BCI qui restent après avoir arrêté vos JOBs interactifs.

Voici un petit script CLP qui permet de faire ça, il vous suffit de le planifier tous les soirs à 23 heures par exemples.

Ici on a utiliser la fonction table QSYS2.ACTIVE_JOB_INFO et la fonction QSYS2.QCMDEXC et on packagé le tout dans un CLP …

PGM
/* Suppression des jobs SSH dans QINTER */
dcl &NBRCURRCD *dec 10
/*  Exécution de la requete SQL */
             RUNSQL     SQL('Create table qtemp/sortie as(SELECT +
                          QSYS2.QCMDEXC(''ENDJOB '' CONCAT +
                          JOB_NAME)  as resultat FROM +
                          TABLE(QSYS2.ACTIVE_JOB_INFO(SUBSYSTEM_LIST_+
                          FILTER => ''QINTER'')) X  where job_type +
                          = ''BCI'') with data') COMMIT(*NONE)
             MONMSG     MSGID(SQL0000) exec(do)
             SNDUSRMSG  MSG('Erreur dans l''exécution de la requête +
                          SQL d''épuration des jobs SSH') +
                          MSGTYPE(*INFO)
             Return
             enddo
  /* Envoi d'un message de fin */
             RTVMBRD    FILE(QTEMP/SORTIE) NBRCURRCD(&NBRCURRCD)
             if cond(&NBRCURRCD > 0) then(do)
             SNDUSRMSG  MSG(%CHAR(&NBRCURRCD) *TCAT ', job(s) SSH +
                          arrêté(s)') MSGTYPE(*INFO)
             enddo
             else do
             SNDUSRMSG  MSG('Pas de Job SSH à Supprimer ') +
                          MSGTYPE(*INFO)
             enddo
ENDPGM

Sur les job de ssh-agent vous pouvez demander le kill directement comme ceci, merci à Julien …

La commande QSH lancer par votre CL qui vous permet de soumettre se présente donc comme ça :

eval "$(ssh-agent -s)" ; // Démarrage de l'agent 
SSH ssh-add /home/jl/.ssh/github ; // Ajout de la clé à l'agent 
ssh -T git@github.com ; // Test de connexion à GitHub 
ssh-agent -k // Arrêter l'agent
, , Visual studio code IBM i Snippets

Vous connaissez tous, au moins de nom l’extension « code for i » qui vous permet d’éditer vos sources RPGLE dans votre éditeur favori qui est publié par notre Ami Liam Allan

Si voulez en savoir plus une vidéo très bien faite de notre ami de Yvon est disponible sur la chaine youtube de Volubis ici

Mais il existe également des extensions créé par d’autres contributeurs en voici une qui est apparu en 2022 « IBM i Snippets », elle a été créée par Andy Youens.

L’installation est très simple à partir de la gestion des extensions

Le principe de cette extension est de vous proposer des exemples de codes (par exemple des squelettes pour du RPGLE ou CLLE).

Son usage est très simple vous devez saisir « !i » dans votre éditeur favori .


Cette extension bouge beaucoup depuis sa sortie et les dernières contributions sont récentes, n’hésitez pas à proposer des exemples de codes si vous en avez qui soit présentable

Nous pensons qu’il y aura de plus en plus de extensions disponibles pour l’IBMI et une amélioration de celle exitantes extensions existantes, il est donc important d’avoir un œil ce mode de développement , en sachant que les « JEUNNNNES » connaissent déjà cet interface