Chiamiamo da C# un programma IBM-i nativo tramite WinSCP/SSH

Chiamare un programma nativo IBM-i (o AS400) da Windows fuori dall’emulatore e con soluzioni automatizzate è sempre stato un incubo: Molti nel tempo hanno sviluppato soluzioni basate sulla DLL di Client Access cwbx.dll, che purtroppo ad oggi è considerata deprecata. Molti hanno preferito usare OleDB con moltissimi limiti (ad esempio non è possibile passare parametri). Altri temerari hanno intrapreso invece lo sviluppo di WS appositi o l’uso di JT400 (ed a loro va tutto il mio rispetto!).

C’è un altro metodo, molto semplice e performante per far iteragire Windows ed il nostro monolite preferito: WinSCP ed SSH!

Perchè chiamare un programma IBM al di fuori del terminale?

In sistemi informativi complessi spesso è necessario far interfacciare componenti Legacy con applicazioni windows.
Orchestrare una serie di programmi/iterazioni complesse, schedulare applicazioni in catene e far comunicare applicazioni differenti è una delle sfide giornaliere che devono affrontare i i system integrator e gli architect solutions.

Prerequisiti

Sul sistema target deve essere presente SSH (il componente è 5733-SC1 Licensed Program Product) configurato e funzionante. Una credenziale d’accesso ed un collegamento aperto sulla porta.

WinSCP e .NET

Molti di voi conoscono WinSCP come un software molto utile per fare file-transfer come alternativa di Filezilla ma in pochi sanno che nella cartella del programma c’è una magnifica DLL, winscpnet.dll che può essere referenziata in un progetto .NET.
Di setuito, un elenco di esempi presi dal sito ufficiale:
Documentazione .net: https://winscp.net/eng/docs/library

Esempio di chiamata in C#

Di seguito il POC da cui sono partito. Io per semplificarmi le cose ho messo parte delle configurazioni in un config file:

using System;
using WinSCP;
using System.Configuration;    
using System.Xml;
using System.Xml.Serialization;
class Programma

{
    public static int Main()
    {
        string server = ConfigurationManager.AppSettings["server"];
        string SshHostKeyFingerprint = ConfigurationManager.AppSettings["SshHostKeyFingerprint"];
        string UserName = ConfigurationManager.AppSettings["UserName"];
        string password = ConfigurationManager.AppSettings["password"];
        string SshPrivateKey = ConfigurationManager.AppSettings["SshPrivateKey"];
        string SshPrivateKeyPassphrase = ConfigurationManager.AppSettings["SshPrivateKeyPassphrase"];
        string LogPath = ConfigurationManager.AppSettings["LogPath"];

        try
        {
            // Setup session options
            SessionOptions sessionOptions = new SessionOptions
            {
                Protocol = Protocol.Sftp,
                HostName = server,
                UserName = UserName,
                //Password = password,
                SshPrivateKeyPath = SshPrivateKey,
                PrivateKeyPassphrase = SshPrivateKeyPassphrase,
                SshHostKeyFingerprint = SshHostKeyFingerprint
            };

            using (Session session = new Session())
            {
                // Connect
                session.SessionLogPath = LogPath;

                session.Open(sessionOptions);

                string CommandAS400 = string.Format(@"system ""CALL PGM(LIBRERIA/PROGRAMMA) PARM('PARAMETRO')""");
                string output;
                output = session.ExecuteCommand(CommandAS400).Output.ToString();
            }

            return 0;
        }
        catch (Exception e)
        {
            Console.WriteLine("Error: {0}", e);
            return 1;
        }
    }
}

Analizziamo il programma nelle componenti su cui prestare attenzione:
La sezione di SessionOptions raccoglie tutte le informazioni di connessione:

Protocol: Protocol.Sftp Questo è un valore fisso
HostName: Nome del server o il suo IP
UserName: Login AS400
Password: Password AS400
SshHostKeyFingerprint: Chiave pubblica del server

In alternativa alla password è possibile usare una chiave privata SSH:

SshPrivateKeyPath: Posizione del file PPK contenente la chiave privata
PrivateKeyPassphrase: Eventuale password della chiave privata

il blocco successivo contiene la chiamata:

il comando va costruito così:

system ""CALL PGM(LIBRERIA/PGM) PARM('PARM')""

La chiamata sfrutta il comando QSH System che permette di chiamare da console POSIX i comandi e programmi di sistema della CLI nativa del sistema operativo.
Invece tramite questo statement:

output = session.ExecuteCommand(CommandAS400).Output.ToString();

Permette di intercettare l’output del programma.
– Se il chiamato è un CLP, può essere usato il comando SNDPGMMSG  MSG(&MSG1)
– Se il chiamato è uno script SH, viene wrappato lo STDout

Usando questo statement poi

session.SessionLogPath = LogPath;

è possibile loggare in maniera molto dettagliata il log di connessione, utile per fare eventuale troubleshooting.

Conclusione

La soluzione si presta per molti scenari, personalmente la uso per chiamare programmi da SSIS o per gestire schedulazioni centralizzate ed orchestrate.

Powershell in un job MSSQL – Error logging – Gestiamo gli errori con le funzioni standard di SQL

Nella cassetta degli attrezzi di un sistemista che si rispetti non può mancare Powershell.
Io personalmente lo adoro e mi permette di automatizzare molti task sistemistici.

Qui sotto riporto un piccolo esempio per integrare uno script Powershell in un job SQL:

$ErrorActionPreference = "Stop"
try{

QUI IL MIO CODICE O LA CHIAMATA AL MIO PS1  

   }
catch{
   Throw
}

Impostato in questa maniera, siamo in grado di intercettare nel job history di SQL le eventuali eccezioni ed errori del nostro script, cosa molto utile se lo si schedula in orario non lavorativo, con alerting automatico in caso di abend (Abnormal End)

Scarichiamo su una tabella MS-SQL i dati degli utenti Active Directory leggendoli sempre da una tabella

Se fai il sistemista e ti diverti a fare reportistica ed incrociare dati tra gli applicativi aziendali e Active Directory, questo è lo script che fa per te.

Lo script powershell sfrutta i seguenti CMDlet:
DBAtools (https://dbatools.io/)
GetADUSER di Microsoft (https://4sysops.com/wiki/how-to-install-the-powershell-active-directory-module/)

Lo script è stato customizzato per essere configurato facilmente e l’ho creato per le mie esigenze.

In breve lo script:
– Legge una tabella con le utenze AD;
– Crea se non esiste la Tabella coi dati di AD
– Interroga AD e memorizza le info in una nuova tabella

Io metto sempre Integrated Security = True per usare l’identity SQL di chi esegue lo script.

$SQLDB = 'IP DEL DB'
$DB = 'NomeDatabase'
$TABLE = 'Tabella da scrivere'
$TABLESRC = 'Tabella da leggere'

$connString = "Server = " + $SQLDB + "; Database = " + $DB +  "; Integrated Security = True"
$QueryText = "SELECT [UTENZAACTIVEDIRECTORY] FROM " + $DB + ".[dbo]." + $TABLESRC

$SqlConnection = new-object System.Data.SqlClient.SqlConnection
$SqlConnection.ConnectionString = $connString
$SqlCommand = $SqlConnection.CreateCommand()

$SqlCommand.CommandText = $QueryText

$DataAdapter = new-object System.Data.SqlClient.SqlDataAdapter $SqlCommand
$dataset = new-object System.Data.Dataset
$DataAdapter.Fill($dataset)

$dataset.Tables[0] | ForEach {

$DataTable = Get-ADUser -identity $_.matricola.Trim() -Properties * |select samaccountname , surname, givenname, mail ,department, enabled |Where-Object {$_.LastLogonDate -ge (get-date).adddays(-400)} | ConvertTo-DbaDataTable
Write-DbaDataTable -SqlInstance $SQLDB -Database $DB -InputObject $DataTable -Table $TABLE -AutoCreateTable
}

Ovviamente è un esempio, con Get-ADUser possono essere fatte centinaia di altre operazioni. Inoltre lo script può essere schedulato su un server, eseguirlo in interattivo o inserirlo in un JOB SQL per un report.

Chiamare da un CL IBMi uno script SH e ricevere un output da esso

CLP (Control Language Program) è un linguaggio di scripting eccezionalmente potente che permette di automatizzare operazioni su IBM-i (conosciuto anche come AS400).

Seppur potente, non copre proprio tutto tutto, e spesso va affiancato ad altri strumenti quali script SH *nix-like, che permette a chi viene da sistemi Linux o Unix di poter creare cose molto interessanti.

Qui di seguito un esempio di programma che intercetta l’output di uno script SH che può essere utilizzato come variabile all’interno di un programma CLP:

PREMESSA: sulla macchina deve essere presente 5770-SS1 – Portable Application Solutions Environment.

Per intercettare il STDOUT va creata una variabile di sistema tramite il comando:

ADDENVVAR ENVVAR(QIBM_QSH_CMD_OUTPUT)  VALUE(STDOUT) LEVEL(*JOB)

Esempio di programma:

PGM                                               
DCLF       FILE(QTEMP/O1)                         
DLTF       FILE(QTEMP/O1)                         
MONMSG     MSGID(CPF2105)                         
CRTPF      FILE(QTEMP/O1) RCDLEN(1000)            
OVRDBF     FILE(STDOUT) TOFILE(QTEMP/O1)          
STRQSH     CMD('SH path dello script')
DLTOVR     FILE(*ALL)                             
RCVF                                              
MONMSG     MSGID(CPF0864)                         
SNDPGMMSG  MSG(&O1)  
ENDPGM                                            

Questo esempio:
– scrive sul file QTEMP/O1 tramite OVRDBF l’output dello script SH
– tramite RCVF del file QTEMP/O1 può essere letto il campo appena scritto come variabile &O1

Letteratura a supporto

https://www.ibm.com/docs/en/i/7.2?topic=reference-calling-qshell-commands-from-i-command-line

https://www.ibm.com/docs/en/i/7.3?topic=i-installing-pase

VBSCRIPT – Zippare ed Unzippare con 7z

In ambito sistemistico, a volte è utile avere tra le proprie armi una funzione per lavorare con i file ZIP. vi allego un comodo esempio da includere nei vostri script

Chiamata alla funzione

    ReturnCode = UnZip(“w:\file*.zip”, ”W:\CartellaOut\”)
    WScript.Echo ”ZIP ” & returnCode

    ReturnCode = Zip(“w:\file*.txt”, ”W:\CartellaOut\file.zip”)
    WScript.Echo ”ZIP ” & returnCode

Ponendo come riportato qui sopra, la funzione restituisce il return code.

Codice di Esempio

Le funzioni sono due: zip ed unzip

Function Zip(sFile,sArchiveName)

  Set oFSO = WScript.CreateObject("Scripting.FileSystemObject")
  Set oShell = WScript.CreateObject("Wscript.Shell")

  '--------Find Working Directory--------
  aScriptFilename = Split(Wscript.ScriptFullName, "\")
  sScriptFilename = aScriptFileName(Ubound(aScriptFilename))
  sWorkingDirectory = Replace(Wscript.ScriptFullName, sScriptFilename, "")
  '--------------------------------------

  '-------Ensure we can find 7z.exe------
  If oFSO.FileExists(sWorkingDirectory & "\" & "7z.exe") Then
    s7zLocation = ""
  ElseIf oFSO.FileExists("C:\Program Files\7-Zip\7z.exe") Then
    s7zLocation = "C:\Program Files\7-Zip\"
  Else
    Zip = "Error: Couldn't find 7z.exe"
    Exit Function
  End If
  '--------------------------------------

  oShell.Run """" & s7zLocation & "7z.exe"" a -tzip -y """ & sArchiveName & """ " _
  & sFile, 0, True   

  If oFSO.FileExists(sArchiveName) Then
    Zip = 1
  Else
    Zip = "Error: Archive Creation Failed."
  End If
End Function

Function UnZip(sArchiveName,sLocation)

  Set oFSO = WScript.CreateObject("Scripting.FileSystemObject")
  Set oShell = WScript.CreateObject("Wscript.Shell")

  '--------Find Working Directory--------
  aScriptFilename = Split(Wscript.ScriptFullName, "\")
  sScriptFilename = aScriptFileName(Ubound(aScriptFilename))
  sWorkingDirectory = Replace(Wscript.ScriptFullName, sScriptFilename, "")
  '--------------------------------------

  '-------Ensure we can find 7z.exe------
  If oFSO.FileExists(sWorkingDirectory & "\" & "7z.exe") Then
    s7zLocation = ""
  ElseIf oFSO.FileExists("C:\Program Files\7-Zip\7z.exe") Then
    s7zLocation = "C:\Program Files\7-Zip\"
  Else
    UnZip = "Error: Couldn't find 7z.exe"
    Exit Function
  End If
  '--------------------------------------

  '-Ensure we can find archive to uncompress-
  If Not oFSO.FileExists(sArchiveName) Then
    UnZip = "Error: File Not Found."
    Exit Function
  End If
  '--------------------------------------

  oShell.Run """" & s7zLocation & "7z.exe"" e -y -o""" & sLocation & """ """ & _
  sArchiveName & """", 0, True
  UnZip = 1
End Function