Am 01.09.2018 durfte ich auf der diesjährigen CIM Lingen einen Vortrag zum Thema „Running Scripts in the Enterprise“ halten. Ein Thema freilich für strategische Beratungen und lange Refactoring-Projekte, das ich versucht habe, in 45 Minuten zu pressen.
Dennoch konnte ich es mir nicht nehmen lassen, zwei Live Demos einzubauen, und da diese nicht in den Folien waren, poste ich die Code Snippets für Windows und Linux ebenfalls hier. Enjoy!
Demo 1: Konfiguration zu Beginn des Skriptes lesen
Sowohl in der Test- als auch in der Produktionsumgebung ist ein Webserver vorhanden, der auf den Namen https://simpleconfig.it-pro-berlin.de hört (da DNS in Test und in Prod aber voneinander unabhängig ist, löst sich dieser Name zu unterschiedlichen Maschinen auf). Dieser Server stellt die Datei config.xml mit dem folgenden Inhalt zur Verfügung:
<scriptconfig> <runglobal>true</runglobal> <sql dbserver="ciminfra.cim.it-pro-berlin.de" dbname="ScriptsDB" dbuser="confread" dbpasswd="confread">Production SQL Database</sql> </scriptconfig>
Die Aufgabe lautet, den Wert von „runglobal“ auszuwerten und, falls er auf „true“ steht, mit den in der nächsten Zeile enthaltenen Verbindungsdaten zu einem SQL Server zu verbinden und aus der Tabelle „SETTINGS“ ddie Einstellung „ENVIRONMENT“ auszulesen. Die Tabelle enthält zwei Spalten: „settingname“ und „settingvalue“.
Hier ist die Lösung in PowerShell. Dieser Code funktioniert unverändert in Windows PS, PSCore auf Windows und PSCore auf Linux.
function Get-CIMConfig { $config_url = "https://simpleconfig.it-pro-berlin.de/config.xml" $res = $null try { $http_response = Invoke-WebRequest -Uri $config_url -TimeoutSec 10 if ($http_response.StatusCode -eq 200) { $config = ($http_response.content) if ($config.scriptconfig.runglobal -eq "true") { $res = New-Object PSObject -Property @{ "dbserver" = $config.scriptconfig.sql.dbserver "dbname" = $config.scriptconfig.sql.dbname "dbuser" = $config.scriptconfig.sql.dbuser "dbpasswd" = $config.scriptconfig.sql.dbpasswd } } } } catch {} $res } #Jedes Skript beginnt mit $cc = Get-CIMConfig if ($cc) { Write-Host "Ausführung gestattet..." $dbconnstr = "Server=$($cc.dbserver);Database=$($cc.dbname);uid=$($cc.dbuser);pwd=$($cc.dbpasswd)" $dbconn = New-Object System.Data.SqlClient.SqlConnection $dbconn.ConnectionString = $dbconnstr $dbconn.Open() | Out-Null $dbcmd = New-Object System.Data.SqlClient.SqlCommand $dbcmd.Connection = $dbconn $dbcmd.CommandText = "SELECT settingvalue FROM SETTINGS WHERE settingname='ENVIRONMENT'" $dbres = $dbcmd.ExecuteReader() $dbtable = New-Object System.Data.DataTable $dbtable.Load($dbres) Write-Host ("Aktuelle Umgebung: {0}" -f $dbtable.Rows[0]['settingvalue']) }
In Produktion würde man natürlich noch ein wenig mehr Überprüfung und Logging einbauen, aber das ist der wesentliche Teil. Dieser Code benötigt keine externen Module oder Bibliotheken.
In VBS ist der Code etwas sperriger, macht aber was er soll und liefert das gewünschte Ergebnis:
strConfigURL = "https://simpleconfig.it-pro-berlin.de/config.xml" Set wshShell = CreateObject("WScript.Shell") strConfigLocal = wshShell.ExpandEnvironmentStrings("%TEMP%") + "\config.xml" wshShell.Run("cmd.exe /C del /q " + strConfigLocal) Set objHTTP = CreateObject("WinHttp.WinHttpRequest.5.1") objHTTP.Open "GET", strConfigURL, False objHTTP.Send If objHTTP.Status = 200 Then Dim objStream Set objStream = CreateObject("ADODB.Stream") With objStream .Type = 1 'adTypeBinary .Open .Write objHTTP.ResponseBody .SaveToFile strConfigLocal .Close End With Set objStream = Nothing End If Set xmlDoc = CreateObject("Microsoft.XMLDOM") xmlDoc.Async = "False" xmlDoc.Load(strConfigLocal) Set colNodes = xmlDoc.SelectNodes("//scriptconfig/runglobal") For Each objNode in colNodes runglobal = objNode.Text Next If runglobal = "true" Then Set colNodes = xmlDoc.SelectNodes("//scriptconfig/sql") For Each objNode in colNodes dbserver = objNode.Attributes.GetNamedItem("dbserver").Text dbname = objNode.Attributes.GetNamedItem("dbname").Text dbuser = objNode.Attributes.GetNamedItem("dbuser").Text dbpasswd = objNode.Attributes.GetNamedItem("dbpasswd").Text Next strDBConnStr = "Provider=SQLOLEDB;Data Source=" & dbserver & ";Initial Catalog=" & dbname & ";User ID=" & dbuser & ";Password=" & dbpasswd End If 'WScript.Echo(strDBConnStr) Set objDBConn = CreateObject("ADODB.Connection") objDBConn.Open(strDBConnStr) Set objRS = CreateObject("ADODB.Recordset") objRS.Open "SELECT settingvalue FROM SETTINGS WHERE settingname='ENVIRONMENT'", objDBConn, 3, 3 objRS.MoveFirst WScript.Echo("Aktuelle Umgebung: " & objRS.Fields.Item("settingvalue")) objRS.Close() objDBConn.Close()
Die Shell auf Linux (geht in POSIX und in BASH) braucht zwei externe Pakete, XMLLINT und FreeTDS, danach funktioniert es mit
#!/bin/sh wget -q "https://simpleconfig.it-pro-berlin.de/config.xml" -O ~/config.xml runglobal=$(xmllint --xpath "//scriptconfig/runglobal/text()" ~/config.xml) if [ $runglobal = true ] then echo "Ausführung gestattet..." dbserver=$(xmllint --xpath "string(//scriptconfig/sql/@dbserver)" ~/config.xml) dbname=$(xmllint --xpath "string(//scriptconfig/sql/@dbname)" ~/config.xml) dbuser=$(xmllint --xpath "string(//scriptconfig/sql/@dbuser)" ~/config.xml) dbpasswd=$(xmllint --xpath "string(//scriptconfig/sql/@dbpasswd)" ~/config.xml) env=$(bsqldb -U $dbuser -P $dbpasswd -S $dbserver -D $dbname -i sico1.sql 2>&-) echo "Umgebung: " $env fi
Die Datei sico1.sql könnte man natürlich dynamisch aus dem Skript generieren, für den Zweck der Demo ist sie aber statisch und hat den folgenden wenig überraschenden Inhalt:
SELECT settingvalue FROM SETTINGS WHERE settingname='ENVIRONMENT'
An der CMD-Shell schaut es natürlich etwas mau aus, dort hat man sich in der Vergangenheit ja immer wieder mit einer selbstgeschriebenen EXE beholfen. Man kann natürlich auch das o.g. VBS-Skript zu Begimn des CMD-Batchscripts bemühen.
Demo 2: Auf einen SYSLOG-Server schreiben
Einträge auf einem SYSLOG-Server erzeugen, das schaffen Skriptsprachen auch. PowerShell (auch Core) nutzt die System.Net-Klassen:
$syslog_server = 'ciminfra.cim.it-pro-berlin.de' $Log_Message = 'PowerShell:TEST' #0=EMERG 1=Alert 2=CRIT 3=ERR 4=WARNING 5=NOTICE 6=INFO 7=DEBUG $Log_Severity = '1' #(16-23)=LOCAL0-LOCAL7 $Log_Facility = '22' # Calculate the priority $Log_Prio = ([int]$Log_Facility * 8) + [int]$Log_Severity $Sender = $env:COMPUTERNAME #Time format the SW syslog understands $TS = Get-Date -Format "MMM dd HH:mm:ss" # Assemble the full syslog formatted message $FullSyslogMessage = "<{0}>{1} {2} {3}" -f $Log_Prio, $TS, $Sender, $Log_Message $FullSyslogMessage # create an ASCII Encoding object $Encoding = [System.Text.Encoding]::ASCII # Convert into byte array representation $ByteSyslogMessage = $Encoding.GetBytes($FullSyslogMessage) $UDPCLient = New-Object System.Net.Sockets.UdpClient $UDPCLient.Connect($syslog_server, 514) $UDPCLient.Send($ByteSyslogMessage, $ByteSyslogMessage.Length) $UDPCLient.Close()
VBS und CMD-Shell müssen hier, wenig überraschend, ohne externe Unterstützung passen, die BASH-Shell hingegen ist der absolute Champion (das gilt aber nicht für die POSIX-Shell):
#!/bin/bash msg="Linux_bash:TESTLOG Definitive" facility=21 #local5 severity=4 #WARN prio=$((8 * $facility + $severity)) syslogserver="ciminfra.cim.it-pro-berlin.de" hname=$(hostname) ts=$(date "+%b %d %H:%M:%S") syslogmsg="<$prio>$ts $hname $msg" echo "$syslogmsg" > /dev/udp/$syslogserver/514
Antworten