Archiv » September, 2018 «

19 | 09 | 2018

PSConf.EU 2019 – Call for Papers ist offen!

Geschrieben von um 9:46 Uhr

Ab heute (und bis zum 09.12.2018) können Vorträge für die PowerShell Konferenz 2019 in Hannover eingereicht werden: HIER.

Also los, Community – reicht eure Themen ein, Hannover wartet!

Tags » , , , , «

+

10 | 09 | 2018

PowerShell Quirks: Values aus der Pipeline, Object Edition

Geschrieben von um 17:51 Uhr

Heute gab es mal wieder eine interessante Frage im TechNet-Forum. Das wollte ich mal näher untersuchen. Es ging darum, dass beim Pipen eines Objektes nach New-Item der Inhalt des erstellten Items das gesamte Objekt, aufgedröselt als Hashtable, enthielt. Im Thread war es eine Datei, aber mit dem Registry-Provider funktionierte es genau so. Das interessante am Value-Parameter ist, dass er Argumente nicht nur nach Namen, sondern auch nach dem Wert bindet:

Doch ist es nur eine Eigenart von New-Item oder ist das Verhalten generell so? Schreiben wir mal eine kleine Funktion, die ihre Argumente, falls sie aus der Pipeline angeflogen kommen, ganz regulär nach Namen bindet:

function Get-MyArgs {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipelineByPropertyName=$true)][string]$MyInput,
        [Parameter(ValueFromPipelineByPropertyName=$true)][string]$MyOtherParm
    )
    Write-Host "Value of MyOtherParm:"
    $MyOtherParm
    Write-Host "Value of MyInput:"
    $MyInput
}

Wenn wir der Funktion jetzt ein Objekt verfüttern, das die gewünschten Properties enthält, werden sie auch ordnungsgemäß gebunden:

$x = New-Object PSCustomObject -Property @{
    MyInput="MyInputValue";
    MyOtherParm="MyOtherParmValue";
    ForeignParm="ShouldNotSeeMe"
}
$x | Get-MyArgs

liefert

Value of MyOtherParm:
MyOtherParmValue
Value of MyInput:
MyInputValue

Akzeptieren wir nur die gleichen Parameter, aber nach dem Wert, wartet eine kleine Überraschung auf uns:

function Get-MyArgs {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$true)][string]$MyInput,
        [Parameter(ValueFromPipeline=$true)][string]$MyOtherParm
    )
    Write-Host "Value of MyOtherParm:"
    $MyOtherParm
    Write-Host "Value of MyInput:"
    $MyInput
}
$x = New-Object PSCustomObject -Property @{
    MyInput="MyInputValue";
    MyOtherParm="MyOtherParmValue";
    ForeignParm="ShouldNotSeeMe"
}
$x | Get-MyArgs

liefert uns

Value of MyOtherParm:
@{MyInput=MyInputValue; MyOtherParm=MyOtherParmValue; ForeignParm=ShouldNotSeeMe}
Value of MyInput:
@{MyInput=MyInputValue; MyOtherParm=MyOtherParmValue; ForeignParm=ShouldNotSeeMe}

Und jetzt kommt der Quirk:

Was passiert aber, wenn wir, wie bei Value in New-Item, beide Bindungen zulassen? Also

function Get-MyArgs {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)][string]$MyInput,
        [Parameter(ValueFromPipelineByPropertyName=$true,ValueFromPipeline=$true)][string]$MyOtherParm
    )
    Write-Host "Value of MyOtherParm:"
    $MyOtherParm
    Write-Host "Value of MyInput:"
    $MyInput
}
$x = New-Object PSCustomObject -Property @{
    MyInput="MyInputValue";
    MyOtherParm="MyOtherParmValue";
    ForeignParm="ShouldNotSeeMe"
}
$x | Get-MyArgs

Anders als bei New-Item, erhalten wir nur die String-Werte der beiden benannten Parameter!
Eine Untersuchung mit

Trace-Command -Name ParameterBinding -Expression {$x | Get-MyArgs} -PSHost

fördert die folgenden Schritte zu Tage:

BIND PIPELINE object to parameters: [Get-MyArgs]
    PIPELINE object TYPE = [System.Management.Automation.PSCustomObject]
    RESTORING pipeline parameter's original values
    Parameter [MyOtherParm] PIPELINE INPUT ValueFromPipeline NO COERCION
    BIND arg [@{MyInput=MyInputValue; MyOtherParm=MyOtherParmValue; ForeignParm=ShouldNotSeeMe}] to parameter [MyOtherParm]
        Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
            result returned from DATA GENERATION: @{MyInput=MyInputValue; MyOtherParm=MyOtherParmValue; ForeignParm=ShouldNotSeeMe}
        BIND arg [@{MyInput=MyInputValue; MyOtherParm=MyOtherParmValue; ForeignParm=ShouldNotSeeMe}] to param [MyOtherParm] SKIPPED
    Parameter [MyInput] PIPELINE INPUT ValueFromPipeline NO COERCION
    BIND arg [@{MyInput=MyInputValue; MyOtherParm=MyOtherParmValue; ForeignParm=ShouldNotSeeMe}] to parameter [MyInput]
        Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
            result returned from DATA GENERATION: @{MyInput=MyInputValue; MyOtherParm=MyOtherParmValue; ForeignParm=ShouldNotSeeMe}
        BIND arg [@{MyInput=MyInputValue; MyOtherParm=MyOtherParmValue; ForeignParm=ShouldNotSeeMe}] to param [MyInput] SKIPPED
    Parameter [MyOtherParm] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION
    BIND arg [MyOtherParmValue] to parameter [MyOtherParm]
        Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
            result returned from DATA GENERATION: MyOtherParmValue
        BIND arg [MyOtherParmValue] to param [MyOtherParm] SUCCESSFUL
    Parameter [MyInput] PIPELINE INPUT ValueFromPipelineByPropertyName NO COERCION
    BIND arg [MyInputValue] to parameter [MyInput]
        Executing DATA GENERATION metadata: [System.Management.Automation.ArgumentTypeConverterAttribute]
            result returned from DATA GENERATION: MyInputValue
        BIND arg [MyInputValue] to param [MyInput] SUCCESSFUL
MANDATORY PARAMETER CHECK on cmdlet [Get-MyArgs]

Bei New-Item ist das Value-Argument allerdings kein [string], sondern ein [object[]] (danke an Martin Binder für den Hinweis, dass dieser Test noch aussteht!). Ändert man das im obigen Beispiel, dann ist das Verhalten so wie bei New-Item auch, und zwar unabhängig davon, ob ein einzelnes [object] oder ein Array davon erwartet wird – es wird in beiden Fällen das ganze Objekt gebunden, sobald ValueFromPipeline=$true auftaucht.
Happy argument-passing!

Tags » , , , «

+

07 | 09 | 2018

HTML5-Client für RDS – große Freude mit kleinen Tücken

Geschrieben von um 20:16 Uhr

Anfang des Jahres hat Microsoft ohne viel Trara einen HTML5-Client für RDS herausgebracht. Ich habe mir das Ding angeschaut und war vom ersten Test ziemlich begeistert. Besonderheiten der neuen Komponente in Kürze:

  • einsetzbar nur für eine komplette RDS-Bereitstellung, nicht für den Zugriff auf einzelne Server
  • ist fest verheiratet mit RDWeb (und ersetzt ihn, wenn man den Webclient produktiv veröffentlicht)
  • erfordert zwingend den RD Gateway (und somit SSL-Zertifikate, denen ohne Wenn und Aber vertraut wird)
  • Drucker- und Zwischenablagenumleitung werden unterstützt, Laufwerke und SmartCard (noch) nicht, von anderer Peripherie ganz zu schweigen
  • Die RDS-Infrastruktur – Broker, RDWeb und Gateway – muss auf Server 2016 oder 2019 laufen (Worker können aus der 2012R2-Generation sein)
  • RDSCALs müssen per User vergeben werden (Device CALs würden sonst sehr schnell verbraucht werden)
  • Nach der offiziellen Guidance (https://docs.microsoft.com/en-us/windows-server/remote/remote-desktop-services/clients/remote-desktop-web-client-admin) muss jeder RDWeb-Server zum Zeitpunkt der Installation Zugang zum Internet, d.h. zur PowerShell Gallery, haben.

Da ich kein großer Freund von Previews bin, habe ich die Testumgebung mit Server 2016 aufgebaut. Deshalb muss ich auf meinem RDWeb-Server zunächst einmal das NuGet-Modul updaten:

Install-Module -Name PowerShellGet -Force

Danach muss die PowerShell geschlossen und neu gestartet werden. So auf die neueste NuGet-Version gebracht, kann das Management-Modul für den Web Client installiert werden:

Install-Module -Name RDWebClientManagement

Das Modul exportiert 12 Cmdlets mit vielversprechender Funktionalität:

Uninstall-RDWebClient
Get-RDWebClientBrokerCert
Import-RDWebClientBrokerCert
Remove-RDWebClientBrokerCert
Get-RDWebClientDeploymentSetting
Set-RDWebClientDeploymentSetting
Find-RDWebClientPackage
Get-RDWebClientPackage
Install-RDWebClientPackage
Publish-RDWebClientPackage
Uninstall-RDWebClientPackage
Unpublish-RDWebClientPackage

Schauen wir mal, was Find-RDWebClientPackage so findet… Das sieht vielversprechend aus:

packageId                       : rd-html5
version                         : 1.0.0
author                          : Microsoft
summary                         : The Remote Desktop Web Client
minRDWebClientManagementVersion : 1.0.0
url                             : https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RE2mzyl
_baseVersion                    : 1.0.0

Doch was ist das? Bereits durch dieses „Finden“ wurde der Ordner „C:\Program Files\RemoteDesktopWeb“ erzeugt, mit einem Ordner „Internal“, auf den ich gar keine Rechte habe! OK, „Deny Everyone Read“ ist ein bisschen plump, aber Internal ist Internal. Drin sind drei Ordner: „Clients“, „Config“ und „Temp“. Alle drei sind noch leer bis auf die „Config\deploymentSettings.js“, die folgenden Inhalt hat:

var DeploymentSettings = {
    "deploymentType":  "rdWeb",
    "suppressTelemetry":  false
}

Echt jetzt? Es wird also früh vorgesorgt, dass die Telemetrie schön an ist. Weiter im Text. Sowohl Get-RDWebClientPackage als auch Install-RDWebClientPackage haben keinen Parameter, mit dem man einen lokalen Speicherort angeben kann, meine RDWeb-Server brauchen also wirklich Zugang zum Internet, damit ich den Web Client installiert bekomme. Im Moment hat mein RDWeb-Server Internet, also installiere ich:

Install-RDWebClientPackage

(das ausgepackte Archiv aus dem Internet liegt nun unter „C:\Program Files\RemoteDesktopWeb\Internal\Clients\4csqnmex.0iv“…)

Import-RDWebClientBrokerCert C:\temp\RDWeb.cer

(das Zertifikat wurde nach „C:\Program Files\RemoteDesktopWeb\Internal\Config\brokercert.cer“ kopiert…)

Publish-RDWebClientPackage -Type Test -Latest

Das war’s! Bereits im IE11 bekomme ich unter https://F.Q.D.N/RDWeb/webclient-test eine moderne Oberfläche angezeigt, verbunden mit dem Hinweis, dass Audio nicht geht: OK, IE11 ist nicht gerade ein HTML5-fähiger Browser…
In Chrome sieht es schon ganz anders aus:

Soweit, so gut. Sieht richtig gut aus, und die Performance haben die RDSGURUS ja auch positiv getestet. Schauen wir mal, was man so einstellen kann, denn oben rechts ist ja ein Zahnrad:

Na, da hat es sich ja richtig gelohnt, einen Dialog dafür zu basteln.
Freunde, diese Telemetrie-Einstellung ist gar nicht so harmlos wie sie aussieht. Wenn ich nämlich die Telemetrie anlasse und der Testumgebung den Internet-Zugang wegnehme, so startet der Web-Client gar nicht erst!

Hmm, das ist nicht schön. Zum Glück betrifft das den Rechner, von dem aus man den Browser öffnet, und nicht die gesamte Bereitstellung. Macht ja auch irgendwie Sinn, dass die am Frontend Telemetrie-Daten sammeln. Könnte diese Zeile sein:

Nicht umsonst hat Microsoft dem PowerShell-Modul das Cmdlet Set-RDWebClientDeploymentSetting mitgegeben, damit kann man nämlich auch ohne Internet-Zugang die Telemetrie ausschalten:

Set-RDWebClientDeploymentSetting -Name SuppressTelemetry -Value $true

Und falls sich jemand die Frage gestellt hat, ob das Ganze auch auf anderen Betriebssystemen geht (z.B. weil er MacOS-User ist und von den ewigen Troubles mit dem RDP Client die Nase voll hat)…

Happy RDSing!

Tags » , , , , , , «

+

06 | 09 | 2018

CIM Lingen 2018 – Folien und Code-Snippets zu meinem Vortrag

Geschrieben von um 16:35 Uhr

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

Folien von der CIM

Und hier die Folien zum Download:

Tags » , , , , , «

+

02 | 09 | 2018

PowerShell-Quirks: Das erste Element bestimmt den Typ, Object Edition

Geschrieben von um 23:19 Uhr

Wir wissen ja schon lange, dass in PowerShell das erste Element in einem Ausdruck in der Regel den Typ des Ergebnisses bestimmt.

(Wem das noch nicht klar ist, probiert bitte das folgende Beispiel aus:

$a = 1
$b = "1"
$a + $b
$b + $a

)

Besonders perfide ist dieses Phänomen allerdings bei inzwischen sehr beliebten Arrays aus PSCustomObjects, die man anfertigt, um das Ganze dann in einheitlich strukturierter Art und Weise auszugeben, z.B. nach Out-GridView oder Export-CSV. Die Konstruktion sieht dann z.B. so aus:

$output = @()
Get-Irgendwas | foreach {
    $object = New-Object PSCustomObject
    if (eine Bedingung) {
        $object | Add-Member -MemberType NoteProperty -Name "eine Property" -Value "ein Wert"
    }
    if (eine andere Bedingung) {
        $object | Add-Member -MemberType NoteProperty -Name "eine andere Property" -Value "ein anderer Wert"
    }
    <# ... usw. #>
    $output += $object
}

oder auch moderner und performanter mit Hashtables:

$output = @()
Get-Irgendwas | foreach {
    $hashtable = @{}
    if (eine Bedingung) {
       $hashtable.Add("eine Property","ein Wert")
    }
    if (eine andere Bedingung) {
        $hashtable.Add("eine andere Property","ein anderer Wert")
    }
    <# ... usw. #>
    $object = New-Object PSCustomObject -Property $hashtable
    $output += $object
}

Für die weitere Verarbeitung entsteht, wenn man nicht aufpasst und das wirklich so schreibt wie oben abgebildet, eine blöde Situation: Das Array enthält Objekte gleichen Typs (PSCustomObject), die aber unterschiedliche Properties haben. Adressiert man die Elemente einzeln, hat man Zugriff auf alle Properties des jeweiligen Objektes. Gibt man das array als Ganzes aus, diktiert das erste Element den Satz an Properties, und die Restlichen sind nicht sichtbar! Probiert es aus:

$a1 = New-Object PSCustomObject -Property @{"prop1"="prop1val1";"prop2"="prop2val1"}
$a2 = New-Object PSCustomObject -Property @{"prop1"="prop1val2";"prop2"="prop2val2"}
$a3 = New-Object PSCustomObject -Property @{"prop1"="prop1val3";"prop3"="prop3val3"}
$a4 = New-Object PSCustomObject -Property @{"prop1"="prop1val4";"prop3"="prop3val4"}
$a5 = New-Object PSCustomObject -Property @{"prop4"="prop4val5";"prop3"="prop3val5"}
$a6 = New-Object PSCustomObject -Property @{"prop4"="prop4val6";"prop3"="prop3val6"}
$arr1 = @($a1,$a2,$a3,$a4,$a5,$a6)
$arr2 = @($a6,$a5,$a4,$a3,$a2,$a1)
$arr1 | Out-GridView
$arr2 | Out-GridView

Das gleiche passiert bei Format-Table, Export-CSV und – das hat mich überrascht – selbst dann, wenn man die Elemente einfach nacheinander ausgibt, also einfach nach dem obigen Beispiel

$a1
$a3
$a5

schreibt. Explizite Out-Default und Out-Host Befehle funktionieren hingegen korrekt und geben für das jeweilige Objekt alle dort enthaltenen Properties aus, ebenso Format-List.

Man sollte also in solchen PSCustomObject-Konstrukten stets jedem Objekt alle Properties mitgeben, auch wenn manche davon leer bleiben. Im obigen Pseudocode-Beispiel würde man folgendes machen:

$output = @()
Get-Irgendwas | foreach {
    $hashtable = @{"eine Property"=$null;"eine andere Property"=$null;...;"die letzte Property"="Vorgabewert"}
    if (eine Bedingung) {
       $hashtable["eine Property"] = "ein Wert"
    }
    if (eine andere Bedingung) {
        $hashtable["eine andere Property"] = "ein anderer Wert"
    }
    <# ... usw. #>
    $object = New-Object PSCustomObject -Property $hashtable
    $output += $object
}

Dann haben alle Objekte im Array alle Properties, ggfls. nur mit Vorgabewerten, und die Ausgabe-Welt ist in Ordnung.
Happy scripting!

Tags » , , «

+

02 | 09 | 2018

CIM Lingen 2018 – das war schön

Geschrieben von um 8:17 Uhr

Gestern hatte ich die Ehre, auf der CIM Lingen 2018 zu „Running Scripts in the Enterprise“ (was sonst?) zu sprechen. Bis dato hatte ich die CIM gar nicht auf dem Schirm gehabt, aber die Emsländer Community-Konferenz hat sich als eine ganz tolle Veranstaltung entpuppt. Trotz einer ordentlichen Größe von 300 Teilnehmern ist die Atmosphäre eines User Group-Treffens unverkennbar vorhanden. An dieser Stelle nochmals vielen Dank an das Orga-Team und natürlich an die Sponsoren, zu denen sowohl mein jetziger als auch mein ehemaliger Arbeitgeber gehören.

Ich habe einige tolle Menschen kennengelernt und einige bisher nur aus dem Online-Geschehen bekannte endlich in Person getroffen. Etwas traurig war es zu sehen, dass langjährigen MVPs und absoluten Grundsteinen der deutschen Microsoft-Community die Auszeichnung mit der Begründung „too little cloud content“ entzogen wurde. Schämt euch, Microsoft! Ihr werdet schon sehen, was ihr davon habt.

Nächstes Jahr soll die (nunmehr 15.) CIM an zwei Tagen stattfinden – 13. + 14.09.2019 (save the date!) und über acht Tracks gehen. Es sind 50 Sessions angekündigt, es ergeben sich also rechnerisch, abzüglich der Opening und der Closing Keynote, drei Slots pro Tag in jedem Track. Somit hätte jeder Slot voraussichtlich eine Länge, die einen deutlich tieferen Einstieg in das jeweilige Thema erlaubt. Drückt mir die Daumen – das wäre genau mein Ding, denn diesmal bin ich etwas aus der Zeit geraten und musste am Ende des 45-minütigen Slots ganz schön hetzen.

Also: auf zur nächsten CIM: #cimlingen #communityrocks

 

Tags » , , , «

+