Freigeben über


Schreiben von PowerShell-Cmdlets für IIS 7.0

von Sergei Antonov

Einführung

Mit PowerShell erhalten IIS-Administratoren ein neues Tool. Der folgende Artikel konzentriert sich auf Verwaltungsaufgaben für IIS 7.0 und höher. PowerShell kann jedoch für vorhandene IIS 6.0-Server verwendet werden.

Dieser Artikel konzentriert sich auf die Verwaltung des IIS-Remoteservers mithilfe von PowerShell auf dem Clientcomputer. Derzeit ist dies nur möglich, wenn Sie den WMI-Anbieter verwenden, den das IIS-Team in Vista und Windows Server® 2008 integriert hat. In diesem Fall müssen keine IIS-Komponenten auf Ihrem Clientcomputer vorhanden sein. WMI stellt die Verbindung mit der tatsächlichen Konfiguration her, die auf dem Remoteserver verfügbar ist.

Hinweis

Sie können auch „Microsoft.Web.Administration“ in PowerShell verwenden, um Verwaltungsfunktionen auszuführen. Dieser Artikel konzentriert sich jedoch nicht auf diese Methode.

Das PowerShell-Team hat einen speziellen Befehl erstellt, der für den Zugriff auf WMI-Objekte verwendet werden soll: „get-wmiobject“. Normalerweise wird ein Objekt zurückgegeben, das in PowerShell erstellt wird und WMI-Eigenschaften und -Methoden verfügbar macht, anstelle des üblichen verwalteten Codeobjekts, das für normale Klassen zurückgegeben wird.

Dieses synthetische Objekt macht Metadaten verfügbar, die im WMI-Namespace definiert sind, nicht Metadaten von „System.Management.ManagementBaseObject“, das als Basis verwendet wird. Dadurch erhält der Benutzer die Namespaceansicht, die vom WMI-Anbieter verfügbar gemacht wurde und das Verwaltungsmodell der konfigurierten Entität widerspiegelt.

Leider funktioniert dieser Befehl nicht mit IIS-Namespaces. Für PowerShell 1.0 unterstützt „get-wmiobject“ nur die Standardauthentifizierungsebene für die DCOM-Remoteverbindung. Das reicht weder für IIS 6.0 noch für IIS 7.0 und höher aus. Wenn Benutzer IIS konfigurieren, kann es erforderlich sein, Kennwörter und andere vertrauliche Daten über die Netzwerkverbindung zu senden, um sie zu bearbeiten und in der Konfiguration zu speichern. Um dies zu unterstützen, benötigen IIS-Anbieter die Authentifizierungsebene „Paketdatenschutz“. Es gibt keine Möglichkeit, diese Anforderung an das Cmdlet „get-wmiobject“ zu übermitteln.

Mit dieser Einschränkung haben wir zwei Optionen:

  • Verwenden Sie PowerShell als generische Skriptschnittstelle für den Namespace „System.Management“. Verwenden Sie PowerShell auch für Folgendes: Schreiben von Skriptcode, der die Verbindung mit dem WMI-Remotenamespace konfiguriert, und Abrufen und Speichern von Verwaltungsdaten mithilfe von System.Management-Objekten, auf die über PowerShell zugegriffen wird. Es ist wie die C#-Programmierung, nur in PowerShell-Sprache. In der Regel funktioniert das gut, aber für den WMI-Fall wendet PowerShell einen speziellen Adapter an, der automatisch alle System.Management-Objekte in synthetische PowerShell-Objekte konvertiert, die WMI-Namespace als primäre API verfügbar machen. Aus diesem Grund müssen wir mit Adapteränderungen umgehen und zusätzlichen Code schreiben, um auf die native System.Management-Unterstruktur zuzugreifen, wodurch das Programmieren schnell unnötig kompliziert wird.

Daher sehen wir uns die andere Option an:

  • Schreiben Sie PowerShell-Cmdlets in C#, und greifen Sie auf die erforderliche Funktionalität im C#-Code zu. In diesem Fall wählen wir alle geeigneten APIs für konfigurierte Entitäten aus. Wir verwenden WMI für den Zugriff auf den Server. Im Vergleich zum gleichen Code in PowerShell ist C# viel effektiver, da der Code nicht jedes Mal geparst und interpretiert werden muss.

PowerShell-Cmdlet

Um mit dem Schreiben von Cmdlets zu beginnen, benötigen Sie einen Clientcomputer, auf dem PowerShell installiert ist. Sie müssen auch das PowerShell SDK installieren oder einfach Verweis-DLLs in den Arbeitsordner kopieren, indem Sie den Trick verwenden, den Jeffrey Snover im PowerShell-Teamblog gepostet hat. Stellen Sie sicher, dass die DCOM-Verbindung auf Ihrem Server vorhanden ist. Die einfachste Möglichkeit, sich davon zu überzeugen, besteht darin, das Hilfsprogramm „wbemtest“ zu starten, das auf jeder Windows-Plattform verfügbar ist, und die Verbindung zu testen.

  1. Starten Sie „wbemtest“.
  2. Klicken Sie auf Verbinden.
  3. Geben Sie die Verbindungsparameter ein:
    • Ersetzen Sie „root\default“ durch „\<Computer>\root\webadministration“, wobei „<Computer>“ der Name Ihres Servers sein muss.
    • Geben Sie die Anmeldeinformationen des Kontos ein, das über Administratorrechte auf dem Server verfügt.
    • Wählen Sie „Paketdatenschutz“ in der Gruppe „Authentifizierungsebene“ aus.
  4. Klicken Sie auf Verbinden. WMI auf Ihrem Clientcomputer stellt eine Verbindung mit dem WMI-Dienst auf Ihrem Servercomputer her. Wenn auf diesen nicht zugegriffen werden kann, wird ein Dialogfeld mit einer Fehlermeldung angezeigt.
  5. Führen Sie eine einfache Aktion aus, die den WMI-Anbieter auf dem Server betreffen, um zu bestätigen, dass er funktioniert. Führen Sie eine Enumeration von Sites aus:
    • Klicken Sie auf die Schaltfläche „Enumerationsinstanzen“, und geben Sie „site“ als Klassennamen ein.
    • Wenn es funktioniert, zeigt das resultierende Dialogfeld eine Liste aller Sites an, die auf Ihrem Server verfügbar sind.

Das PowerShell-Cmdlet ist einfach eine Assembly mit verwaltetem Code, die nach formalen Regeln implementiert ist, die im PowerShell SDK dokumentiert sind. Sie finden sie online.

Bevor Sie Code schreiben, ist es vorteilhaft, einen Plan zu haben.

Implementieren Sie zunächst ein Cmdlet, das alle IIS-Sites auf dem Remoteserver aufzählt. Dieses Cmdlet gibt ein Array mit Siteobjekten zurück, das das IIS-Konfigurationselement mit Eigenschaften darstellt, die für die Site definiert sind. Wir fügen einige zusätzliche Eigenschaften hinzu, die für dieses Objekt nützlich sind.

Das Cmdlet soll wie folgt aussehen:

get-iissite –computer somecomputer –name somesite –credential $(get-credential)

Wenn die Anmeldeinformationen nicht programmgesteuert an das Cmdlet übergeben werden, erstellt PowerShell ein Dialogfeld, in dem der Benutzername und das Kennwort für den Remoteserver angefordert werden.

Um das Siteobjekt vom Remotecomputer abzurufen, müssen wir die folgenden Parameter aus unserem Cmdlet angeben:

public string Computer;
public string Name;
public PSCredential Credential;

Alle diese Parameter sind öffentliche Eigenschaften in der Cmdlet-Klasse, die mit dem Parameter-Attribut versehen sind.

Implementieren Sie das erste Cmdlet. Da „get-iissite“ nicht unser letzter Befehl ist, sind zwei Dinge ratsam: das Separieren des Codes, der für die Verbindung mit dem Server mit der übergeordneten Klasse „RemotingCommand“ verantwortlich ist, und das Erben des Cmdlets von dieser Klasse.

using System;
using System.Net;
using System.Management;
using System.Management.Automation;
using System.ComponentModel;
using System.Security;
 
namespace Microsoft.Samples.PowerShell.IISCommands
{
    public class RemotingCommand : PSCmdlet
    {
        private string computer = Environment.MachineName;
        [Parameter(
           ValueFromPipeline = true,
           ValueFromPipelineByPropertyName = true)]
        [ValidateNotNullOrEmpty]
        public string[] Computer
        {
            get { return computer; }
            set { computer = value; }
        }
 
        private PSCredential credential = null;
        [Parameter(
           ValueFromPipeline = true,
           ValueFromPipelineByPropertyName = true)]
        [CredentialAttribute]
        public PSCredential Credential
        {
            get { return credential; }
            set { credential = value; }
        }

        protected ManagementScope GetScope(string computerName)
        {
            ConnectionOptions connection = new ConnectionOptions();
            connection.Username = Credential.UserName;
            connection.Password = Credential.GetNetworkCredential().Password;
            connection.Impersonation = ImpersonationLevel.Impersonate;
            connection.Authentication = AuthenticationLevel.PacketPrivacy;
            ManagementScope scope = new ManagementScope(
"\\\\" + computerName + "\\root\\webadministration", connection);
            return scope;
        }
 
        protected override void EndProcessing()
        {
            if (null == credential)
            {
                // Check variable first 
               object varCred = GetVariableValue("IISCredential");
                if (varCred != null && varCred.GetType() == typeof(PSObject))
                {
                    credential = ((PSObject)varCred).BaseObject as PSCredential;
                }
                if (null == credential)
                {
                    // use credential of current user or process 
                   SecureString ss = new SecureString();
     foreach (char c in  CredentialCache.DefaultNetworkCredentials.Password.ToCharArray())
                    {
                        ss.AppendChar(c);
                    }
                    credential = new PSCredential(
                        CredentialCache.DefaultNetworkCredentials.UserName, ss);
                }
            }
       }
 
       protected ManagementClass CreateClassObject(
          string computerName, 
          string classPath )
       {
            return new ManagementClass(
                GetScope(computerName), 
                new ManagementPath(classPath), 
                new ObjectGetOptions()
             );
        }
    }

Die Klasse „RemotingCommand“ enthält Parameter und Methoden, die für die Verbindung erforderlich sind.

  • „GetScope()“ gibt das System.Management-Objekt zurück, das alle Informationen enthält, die für die Verbindung mit dem Remotenamespace erforderlich sind. Sehen Sie sich die Eigenschaft „connection.Authentication“ an. Sie wird in „AuthenticationLevel.PacketPrivacy“ initialisiert. Dies ist eine verbindliche Anforderung. Andernfalls lehnt WMI die Verbindung ab.
  • Die Methode „CreateClassObject()“ ist eine Hilfsmethode, die den Verbindungsbereich verwendet, um eine angegebene Klasse basierend auf dem WMI-Remotenamespace zu erstellen.
  • Die Methode „EndProcessing()“ ist die Standardmethode, die jede Cmdlet-Klasse implementieren soll. Sie wird von PowerShell aufgerufen, wenn unser Cmdlet verarbeitet wird. Die Implementierung von „EndProcessing()“ versucht, die Anmeldeinformationseigenschaft auszufüllen, wenn sie leer ist. Beim ersten Versuch werden die Anmeldeinformationen aus der externen Variablen „IISCredential“ (zur Vereinfachung) abgerufen.

In einer PowerShell-Sitzung kann der Benutzer den Benutzernamen und das Kennwort in dieser Variablen speichern und mehrmals in mehreren Befehlen verwenden. Wenn diese Variable nicht definiert ist oder einen ungeeigneten Objekttyp enthält, ruft der Code die Anmeldeinformationen des aktuellen Benutzers oder Prozesses ab. Es funktioniert, wenn der Benutzer diesen Befehl lokal auf dem Server unter Verwendung des Administratorkontos ausführt. In diesem Fall müssen wir überhaupt keine Anmeldeinformationen eingeben. Für jede Schleife in diesem Code gibt es einen bekannten Trick zum Konvertieren des Kennworts aus einer Zeichenfolge in „SecureString“.

Jetzt implementieren wir „get-iissite“.

[Cmdlet(VerbsCommon.Get, "IISSite")]
    public class GetSiteCommand : RemotingCommand
    {
        private string name = null;
        [Parameter(
           Position = 0,
           ValueFromPipeline = true,
           ValueFromPipelineByPropertyName = true)]
        public string Name
        {
            get { return name; }
            set { name = value; }
        }
 
        protected override void EndProcessing()
        {
            base.EndProcessing();
 
            ManagementObjectCollection sites = CreateClassObject(computerName, "Site").GetInstances();
            foreach (ManagementObject site in sites)
            {
                string siteName = site.GetPropertyValue("Name") as string;
                if (Name != null)
                {
                    if (siteName.Equals(Name, StringComparison.InvariantCultureIgnoreCase))
                    {
                        WriteObject(siteName);
                        break;
                    }
                }
                else 
                {
                    WriteObject(siteName);
                }
            }
        }
    } //GetSiteCommand 
    // 
    // [RunInstaller(true)] 
    // public class IISDemoCmdSnapIn : PSSnapIn {…} 
    // 
} // Microsoft.Samples.PowerShell.IISCommands

Zuerst gibt der Befehl nur Sitenamen zurück. Wenn der Benutzer eine bestimmte Site möchte, muss er dem Befehl den Namen dieser Site angeben. Andernfalls werden alle Sites auf diesem bestimmten Computer zurückgegeben.

Um den Befehl abzuschließen, müssen Sie die Implementierung der Klasse hinzufügen, die von „PSSnapin“ geerbt wurde. Diese Klasse wird verwendet, um unsere Befehle zu registrieren. Sie ist nicht speziell für IIS vorgesehen. Den vollständigen Code finden in der Quelldatei „IISDemoCmd.cs“.

Führen Sie das Cmdlet jetzt aus, und beobachten Sie, wie es funktioniert. Sie können das in Visual Studio tun, aber es reicht aus, es über die Befehlszeile auszuführen. Nehmen Sie an, dass Sie PowerShell-Verweis-DLLs im Ordner c:\sdk gespeichert haben. Die folgende Befehlszeile erstellt das Cmdlet in „IISDemoCmd.dll“ und speichert es im selben Ordner, in dem sich die Quelldatei befindet.

%windir%\Microsoft.NET\Framework\v2.0.50727\csc /t:library /r:c:\sdk\system.management.automation.dll IISDemoCmd.cs

Jetzt müssen Sie den Befehl registrieren und ihn in PowerShell hinzufügen. Der Ablauf wird in der PowerShell-Programmierreferenz beschrieben. Starten Sie PowerShell, und führen Sie die folgenden Befehle über denselben Ordner aus, in dem Sie die Cmdlet-DLL erstellt haben.

>set-alias installutil $env:windir\Microsoft.NET\Framework\v2.0.50727\installutil
>installutil iisdemocmd.dll
>add-pssnapin IISDemoCmdSnapIn

Dadurch wird das Cmdlet zur ausgeführten Instanz von PowerShell hinzugefügt. Speichern Sie diese Befehlszeilen in einer Skriptdatei. Sie werden sie erneut verwenden, während Sie mit der Arbeit an den Cmdlets fortfahren. Sie finden dieses Skript in der Datei „demo_install.ps1“.

Überprüfen Sie, ob der Befehl verfügbar ist:

>get-command Get-IISSite
 
CommandType     Name                            Definition
-----------     ----                            ----------
Cmdlet          Get-IISSite                     Get-IISSite [[-Name] <String...

Probieren Sie ihn jetzt aus. Nehmen Sie an, dass Sie mithilfe des lokalen Administratorkontos eine Verbindung mit dem Computer „test_server“ herstellen.

>Get-IISSite -computer test_server -credential $(get-credential administrator)
 
Default Web Site
Foo
Bar

Diese Befehlszeile empfängt das Anmeldeinformationsobjekt vom Cmdlet „get-credential“, das mit dem Benutzer interagiert, um das Kennwort zu beziehen. Es ist auch möglich, die Anmeldeinformationen programmgesteuert zu generieren, aber Sie müssen das Kennwort in das Skript eingeben, was überhaupt nicht sicher ist.

>$global:iiscredential = new-object System.Management.Automation.PsCredential "Administrator",$(convertto-securestring "password" -asplaintext -force)

Mit diesem Befehl werden die Anmeldeinformationen in der globalen Variablen „$iiscredential“ gespeichert, und das Cmdlet verwendet sie automatisch. In echten Situationen ist es jedoch besser, die Anmeldeinformationen mithilfe des Befehls „get-credential“ in einer Variablen zu speichern: $global:iiscredential = get-credential Administrator.

Dieser Befehl wird nun erneut ausgeführt.

>Get-IISSite -computer test_server
Default Web Site
Foo
Bar

Die gesamte Infrastruktur ist vorhanden. Kehren Sie nun zum Befehl zurück, und fügen Sie die restlichen Daten von der Site hinzu.

Hinzufügen von Konfigurationsdaten zur Site

Wir müssen das Objekt von „ManagementBaseObject“ in „PSObject“ konvertieren und es an PowerShell zurückgeben. PSObject ist ein Freiformcontainer, der mit verschiedenen Arten von Daten gefüllt werden kann. Wir verwenden PSNoteProperty-Typ. Halten Sie den Cmdlet-Code übersichtlich, und fügen Sie eine neue Klasse hinzu, die für die Konvertierung zuständig ist.

class ObjectConverter
{
    public static PSObject ToPSObject(
        ManagementBaseObject source
        )
    {
        PSObject obj = new PSObject();
        foreach (PropertyData pd in source.Properties)
        {
            if (pd.Value.GetType() == typeof(System.Management.ManagementBaseObject))
            {
                obj.Properties.Add(new PSNoteProperty(
                    pd.Name, ObjectConverter.ToPSObject(pd.Value as ManagementBaseObject)
                    ));
            }
            else if (pd.Value.GetType() == typeof(ManagementBaseObject[]))
            {
                ManagementBaseObject[] ar = pd.Value as ManagementBaseObject[];
                PSObject[] psar = new PSObject[ar.Length];
                for (int i = 0; i < ar.Length; ++i)
                {
                    psar[i] = ObjectConverter.ToPSObject(ar[i]);
                }
                obj.Properties.Add(new PSNoteProperty(pd.Name, psar));
            }
            else 
           {
                obj.Properties.Add(new PSNoteProperty(pd.Name, pd.Value));
            }
        }
        return obj;
    }
}

Dieser Code greift auf komplexe Eigenschaften zurück und fügt dem resultierenden „PSObject“ einfache Eigenschaften wie „PSNoteProperty“ hinzu. Außerdem fügen wir eine dedizierte Methode hinzu, mit der die Konvertierung in das Cmdlet durchgeführt wird. Diese Methode konvertiert alle WMI-Daten und fügt zwei weitere Eigenschaften hinzu: den Computernamen und die Anmeldeinformationen, die zum Abrufen der Verbindung mit diesem Computer verwendet wurden. Das hilft, die einzelnen Sites von anderen Objekten in der PowerShell-Sitzung zu unterscheiden.

private PSObject ConstructPSSite(
    string computerName,
    ManagementObject site)
{
    PSObject pssite = ObjectConverter.ToPSObject(site);
    pssite.Properties.Add(new PSNoteProperty("Computer", computerName));
    pssite.Properties.Add(new PSNoteProperty("Credential", Credential));
    return pssite;
}

Ersetzen Sie den Sitenamen, den wir an PowerShell zurückgegeben haben, durch ein vollständiges Objekt. Die Methode „EndProcessing()“ im Cmdlet sieht nun wie folgt aus:

protected override void EndProcessing()
{
    base.EndProcessing();

    ManagementObjectCollection sites = CreateClassObject(Computer, "Site").GetInstances();
    foreach (ManagementObject site in sites)
    {
        string siteName = site.GetPropertyValue("Name") as string;
        if (Name != null)
        {
            if (siteName.Equals(Name, StringComparison.InvariantCultureIgnoreCase))
            {
                WriteObject(ConstructPSSite(Computer, site));
                break;
            }
        }
        else 
       {
            WriteObject(ConstructPSSite(Computer, site));
        }
    }
}

Wenn wir den Build und die Registrierung wiederholen und den Befehl erneut ausführen, werden weitere Daten zur Site angezeigt:

> get-iissite -computer test-server "default web site"
 
ApplicationDefaults        : @{ApplicationPool=; EnabledProtocols=http; Path=}
Bindings                   : {@{BindingInformation=*:80:; Protocol=http}}
Id                         : 1
Limits                     : @{ConnectionTimeout=00000000000200.000000:000; Max
                             Bandwidth=4294967295; MaxConnections=4294967295}
LogFile                    : @{CustomLogPluginClsid=; Directory=%SystemDrive%\i
                             netpub\logs\LogFiles; Enabled=True; LocalTimeRollo
                             ver=False; LogExtFileFlags=2199503; LogFormat=2; P
                             eriod=1; TruncateSize=20971520}
Name                       : Default Web Site
ServerAutoStart            : True
TraceFailedRequestsLogging : @{Directory=%SystemDrive%\inetpub\logs\FailedReqLo
                             gFiles; Enabled=False; MaxLogFiles=50}
VirtualDirectoryDefaults   : @{AllowSubDirConfig=True; LogonMethod=3; Password=
                             ; Path=; PhysicalPath=; UserName=}
Computer                   : test-server 
Credential                 : System.Management.Automation.PSCredential

Wenn Sie diese mit dem WMI-Schema für die Site vergleichen, sehen Sie, dass alle Daten jetzt verfügbar sind. Darüber hinaus sind zusätzliche Eigenschaften vorhanden, die wir im Cmdlet hinzugefügt haben. Auf alle Eigenschaften kann über PowerShell über die Punktnotation zugegriffen werden.

> $sites = get-iissite -computer test-server 
>$sites[0]

ApplicationDefaults        : @{ApplicationPool=; EnabledProtocols=http; Path=}
Bindings                   : {@{BindingInformation=*:80:; Protocol=http}}
Id                         : 1
Limits                     : @{ConnectionTimeout=00000000000200.000000:000; Max
                             Bandwidth=4294967295; MaxConnections=4294967295}
LogFile                    : @{CustomLogPluginClsid=; Directory=%SystemDrive%\i
                             netpub\logs\LogFiles; Enabled=True; LocalTimeRollo
                             ver=False; LogExtFileFlags=2199503; LogFormat=2; P
                             eriod=1; TruncateSize=20971520}
Name                       : Default Web Site
ServerAutoStart            : True
TraceFailedRequestsLogging : @{Directory=%SystemDrive%\inetpub\logs\FailedReqLo
                             gFiles; Enabled=False; MaxLogFiles=50}
VirtualDirectoryDefaults   : @{AllowSubDirConfig=True; LogonMethod=3; Password=
                             ; Path=; PhysicalPath=; UserName=}
Computer                   : test-server 
Credential                 : System.Management.Automation.PSCredential
 
>$sites[0].Limits

ConnectionTimeout                        MaxBandwidth            MaxConnections
-----------------                        ------------            --------------
00000000000200.000000:000                  4294967295                4294967295
 
> $sites[0].Limits.MaxBandwidth
4294967295

Unsere Fortschritte sind gut, aber nicht gut genug. Die WMI-Site verfügt auch über Methoden. Versuchen Sie daher, diese ebenfalls hinzuzufügen. Das ist in PowerShell einfach: Benennen Sie die Methode, und teilen Sie PowerShell mit, wo sich der Code befindet. Wir fügen Methoden des PSCodeMethod-Typs hinzu. Um den Code für die Methoden beizubehalten, fügen wir die Klasse „SiteMethods“ hinzu.

public class SiteMethods
{
    static public void Start(PSObject site)
    {
        InvokeMethod(site, "Start");
    }
    static public void Stop(PSObject site)
    {
        InvokeMethod(site, "Stop");
    }

    static public string GetStatus(PSObject site)
    {
        uint status = (uint)InvokeMethod(site, "GetState");
        string statusName =
            status == 0 ? "Starting" :
            status == 1 ? "Started" :
            status == 2 ? "Stopping" :
            status == 3 ? "Stopped" : "Unknown";
        return statusName;
    }

    static private object InvokeMethod(PSObject site, string methodName)
    {
        string computerName = site.Properties["Computer"].Value as string;
        string siteName = site.Properties["Name"].Value as string;
        PSCredential credential = site.Properties["Credential"].Value as PSCredential;
        ConnectionOptions connection = new ConnectionOptions();
        connection.Username = credential.UserName;
        connection.Password = credential.GetNetworkCredential().Password;
        connection.Impersonation = ImpersonationLevel.Impersonate;
        connection.Authentication = AuthenticationLevel.PacketPrivacy;
        ManagementScope scope = new ManagementScope(
            "\\\\" + computerName + "\\root\\webadministration", connection);
        string sitePath = "Site.Name=\"" + siteName + "\"";
        ManagementObject wmiSite = new ManagementObject(
            scope, new ManagementPath(sitePath), new ObjectGetOptions());
        return wmiSite.InvokeMethod(methodName, new object[] { });
    }
}

Wie Sie sehen, erstellt dieser Code ein WMI-Objekt für die Site und ruft WMI-Methoden für dieses Objekt auf. Dieser Code verwendet zwei zusätzliche Eigenschaften, die wir der Site hinzugefügt haben. Mit dieser Klasse können wir die Methode „ConstructPSSite“ erweitern. Außerdem müssen wir einen Verweis auf den System.Reflection-Namespace hinzufügen.

private PSObject ConstructPSSite(
    string computerName,
    ManagementObject site)
{
    PSObject pssite = ObjectConverter.ConvertSiteToPSObject(site);
    pssite.Properties.Add(new PSNoteProperty("Computer", computerName));
    pssite.Properties.Add(new PSNoteProperty("Credential", Credential));
    Type siteMethodsType = typeof(SiteMethods);
    foreach (MethodInfo mi in siteMethodsType.GetMethods())
    {
        if (mi.Name.Equals("Start", StringComparison.InvariantCultureIgnoreCase))
        {
            pssite.Methods.Add(new PSCodeMethod("Start", mi));
        }
        if (mi.Name.Equals("Stop", StringComparison.InvariantCultureIgnoreCase))
        {
            pssite.Methods.Add(new PSCodeMethod("Stop", mi));
        }
        if (mi.Name.Equals("GetStatus", StringComparison.InvariantCultureIgnoreCase))
        {
            pssite.Properties.Add(new PSCodeProperty("Status", mi));
        }
    }
    return pssite;
}

Zusätzlich zu den hinzugefügten Methoden gibt es eine dynamische Eigenschaft, „Status“. Sie verhält sich auf die gleiche Weise wie Eigenschaften in C#-Klassen. Sie ist eine Funktion, die aufgerufen wird, wenn PowerShell ihren Wert benötigt. Der Code ist sehr einfach, da wir auf Methoden aus derselben Assembly wie unser Cmdlet verweisen. Nichts verhindert das Laden anderer Assemblys und das Abrufen der Informationen zu den Methoden ihrer Klassen. Wenn diese Methoden über die richtige Signatur verfügen, verwendet PowerShell sie auf die gleiche Weise.

Das Objekt sieht nun wie folgt aus:

>$s = get-iissite "Default Web Site" –computer test-server 
> $s | get-member

   TypeName: System.Management.Automation.PSCustomObject

Name                       MemberType   Definition
----                       ----------   ----------
Start                      CodeMethod   static System.Void Start(PSObject site)
Stop                       CodeMethod   static System.Void Stop(PSObject site)
Status                     CodeProperty System.String Status{get=GetStatus;}
Equals                     Method       System.Boolean Equals(Object obj)
GetHashCode                Method       System.Int32 GetHashCode()
GetType                    Method       System.Type GetType()
ToString                   Method       System.String ToString()
ApplicationDefaults        NoteProperty System.Management.Automation.PSObjec...
Bindings                   NoteProperty System.Management.Automation.PSObjec...
Computer                   NoteProperty System.String Computer=iissb-101
Credential                 NoteProperty System.Management.Automation.PSCrede...
Id                         NoteProperty System.UInt32 Id=1
Limits                     NoteProperty System.Management.Automation.PSObjec...
LogFile                    NoteProperty System.Management.Automation.PSObjec...
Name                       NoteProperty System.String Name=Default Web Site
ServerAutoStart            NoteProperty System.Boolean ServerAutoStart=True
TraceFailedRequestsLogging NoteProperty System.Management.Automation.PSObjec...
VirtualDirectoryDefaults   NoteProperty System.Management.Automation.PSObjec...
 
>$s.Status
Started
> $s.Stop()
> $s.Status
Stopped
> $s.Start()
> $s.Status
Started

Mit der Möglichkeit, den Objekten Methoden und dynamische Eigenschaften hinzuzufügen, können wir das synthetisieren, was wir für jede Situation benötigen. Zusätzlich zu den Methoden und Eigenschaften, die im Cmdlet hinzugefügt wurden, können wir dem Skript weitere hinzufügen, ohne dass C#-Code verwendet werden muss.

Es ist auch möglich, die Definition des Objekts aus XML zu laden. Ein guter Kandidat für zusätzliche Eigenschaften sind Daten, die von IIS über perf-Zähler im Zusammenhang mit der Site verfügbar gemacht werden, z. B. die Gesamtanzahl der verarbeiteten Anforderungen. Auf diese Daten kann problemlos direkt über den verwalteten Code zugegriffen werden. Es ist nicht erforderlich, WMI zu verwenden.

Aufrufen eines Cmdlets über ein anderes Cmdlet

Das Abrufen von Siteobjekten ist wichtig, aber wir benötigen mehr, z. B. das Schreiben eines Befehls zum Hinzufügen einer neuen Site. Zum Erstellen von Sites können wir die abstrakte Methode „Create()“ verwenden, die in der Site-Klasse im WMI-Namespace „WebAdministration“ definiert ist. Das Cmdlet sieht wie folgt aus:

>add-iissite –name <siteName> -computer <serverName> -credential <credential> -bindings <array-of-bindings> –homepath <path> -autostart

Wir haben dieselben Parameter wie in der Create-Methode definiert. Darüber hinaus sollte der Befehl die Parameter „–whatif“ und „–passthru“ unterstützen. Das erste zeigt das Ergebnis der Befehlsausführung, nimmt jedoch keine Änderungen vor. Der zweite weist den Befehl an, das Ergebnis in der Pipeline auszugeben. Es wird dringend empfohlen, diese beiden Parameter in destruktiven Befehlen zu verwenden. Um das Cmdlet „–whatif“ zu unterstützen, muss die Klasse mit dem Attribut „SupportsShouldProcess = true“ versehen werden.

Hier ist Teil des Codes (Sie finden den gesamten Code in „iisdemocmd.cs“):

[Cmdlet(VerbsCommon.Add, "IISSite", SupportsShouldProcess = true)]
public class AddSiteCommand : RemotingCommand
{
    //… 
   private SwitchParameter passThru = new SwitchParameter(false);
    [Parameter]
    public SwitchParameter PassThru
    {
        get { return passThru; }
        set { passThru = value; }
    }

    protected override void EndProcessing()
    {
        base.EndProcessing();
        if (ShouldProcess(string.Format("{0} bound to {1} on {2}", name, bindings.ToString(), rootFolder)))
        {
            object[] args = new object[4];
            args[0] = Name;
            ManagementBaseObject[] mbarr = new ManagementBaseObject[bindings.Length];
            for (int b = 0; b < bindings.Length; ++b)
            {
                mbarr[b] = ObjectConverter.ToManagementObject(
                    GetScope(Computer), "BindingElement", bindings[b]);
            }
            args[1] = mbarr;
            args[2] = rootFolder;
            args[3] = autoStart;
            ManagementClass siteClass = CreateClassObject(Computer, "Site");
            try 
           {
                siteClass.InvokeMethod("Create", args);
            }
            catch (COMException comEx)
            {
                WriteError(new ErrorRecord(comEx, comEx.Message, ErrorCategory.InvalidArgument, Name));
            }
            if (PassThru.IsPresent)
            {
                string getSiteScript = "get-iissite" 
                        + " -name " + Name
                        + " -computer " + Computer
                        + " -credential $args[0]";
                this.InvokeCommand.InvokeScript(
                    getSiteScript, false, PipelineResultTypes.Output, null, Credential);
            }
        }
    }
}

Dieser Code verwendet eine neue Methode in der ObjectConverter-Klasse, um ein Bindungsarray zu generieren. Die Methode „ToManagementObject()“ konvertiert Eingabeparameter, zum Beispiel „PSObject“ oder „Hashtable“, in die Instanz der ManagementBaseObject-Klasse. Da der Aufruf von „Create“ selbst mit korrekten Parametern fehlschlägt, wenn die Site mit diesen Parametern bereits vorhanden ist, rufen wir diese Methode in „try/catch“ auf.

Wenn das Cmdlet abschließend überprüft, ob der vom Benutzer angegebene Parameter „–passthru“ vorhanden ist, ruft es PowerShell auf, um ein Skript auszuführen, das diese neue Site zurückgibt. In diesem Skript rufen wir unseren Befehl „get-iissite“ auf und verwenden erneut die Parameter, die an den aktuellen Befehl übergeben wurden. „InvokeScript“ fügt das Ergebnis wie gefordert in die Pipeline ein, sodass keine weiteren Schritte erforderlich sind. Dies ist ein Beispiel dafür, wie wir einen Rückruf zu PowerShell ausführen können, indem formatierte Befehlszeilen statisch oder dynamisch übergeben werden. Natürlich ist es möglich, sie als C#-Code zu schreiben. Das erfordert jedoch entweder das Ausschneiden und Einfügen großer Teile von „GetSiteCommand“ oder das Umstrukturieren und Refactoring des Namespaces.

Am Anfang der Methode „EndProcessing()“ sehen sie den Aufruf von „ShouldProcess()“. Auf diese Weise wird der Parameter „–whatif“ unterstützt. Wenn der Benutzer diesen Parameter übergibt, gibt diese Methode Text aus, der als Parameter an sie übergeben wird, und die Rückgabe ist „false“. Alle Aktionen, die die Umgebung ändern können, müssen ausgeführt werden, wenn dieser Aufruf „true“ zurückgibt. PowerShell verfügt über andere Parameter, die mit dem Benutzer interagieren und um Bestätigung bitten können, bevor eine Aktion ausgeführt wird. „ShouldProcess()“ wird als Ergebnis dieser Bestätigung zurückgegeben.

Testen Sie den neuen Befehl folgendermaßen:

> add-iissite Foo @{Protocol="http"; BindingInformation="*:808"} e:\inetpub\demo -computer test-server -whatif

What if: Performing operation "Add-IISSite" on Target "Foo bound to System.Management.Automation.PSObject[] on e:\inetpub\demo".

So funktioniert „–whatif“. Wir erhalten eine ziemlich kryptische Nachricht darüber, was passiert, wenn dieser Befehl ausgeführt wird. Um sie lesbarer zu machen, müssen wir sie richtig formatieren. Parameterbindungen werden in der Befehlszeile als Hashtabelle eingegeben und an das Cmdlet als „Hashtable“ in „PSObject“ übergeben. Um aussagekräftigen Text daraus zu erzeugen, müssen wir intelligenteren Code hinzufügen. Die Standardmethode „ToString()“ gibt nur den Klassennamen zurück.

Fügen Sie diesen Textblock anstelle der ShouldProcess()-Zeile ein:

StringBuilder bindingText = new StringBuilder("(");
foreach (PSObject b in bindings)
{
    Hashtable ht = b.BaseObject as Hashtable;
    foreach (object key in ht.Keys)
    {
        string bstr = String.Format("{0}={1}",
            key.ToString(), ht[key].ToString());
        bindingText.Append(bstr + ",");
    }
    bindingText.Remove(bindingText.Length - 1, 1);
    bindingText.Append(";");
}
bindingText.Remove(bindingText.Length - 1, 1);
bindingText.Append(")");
if (ShouldProcess(string.Format("{0} bound to {1} on {2}", name, bindingText.ToString(), rootFolder)))

Nachdem das Cmdlet kompiliert und ausgeführt wurde, wird die folgende Ausgabe angezeigt:

> add-iissite Foo @{Protocol="http"; BindingInformation="*:888"} e:\inetpub\demo -computer test-server -whatif

What if: Performing operation "Add-IISSite" on Target "Foo bound to (BindingInformation=*:888,Protocol=http) on e:\inetpub\demo".

Diese ist viel verständlicher. Aus diesem neuen Code ist auch klar, warum wir „Hashtable“ in der Methode „ToManagementObject()“ verarbeiten müssen. Dies ist ein gängiger Typ in PowerShell zum Übergeben strukturierter Parameter.

Führen Sie nun den folgenden Befehl aus:

> add-iissite Foo @{Protocol="http"; BindingInformation="*:888"} e:\inetpub\demo -computer test-server -passthru | format-table Name,Status

Name                                    Status
----                                    ------
Foo                                     Stopped
 
> get-iissite -computer sergeia-a | format-table name,status

Name                                    Status
----                                    ------
Default Web Site                        Started
Foo                                     Stopped

Der erste Befehl hat die Site auf dem Remoteserver erstellt, abgerufen und an die Pipeline übergeben. Um sicherzustellen, dass das ordnungsgemäß abgelaufen ist, haben wir eine Liste der Sites abgerufen, und tatsächlich ist die neue Site verfügbar. Standardmäßig versucht der Server, die Site zu starten, es sei denn, wir fügen den Parameter „–AutoStart false“ hinzu. Wenn es ein Problem in den Parametern gibt, z. B. dass der Server den Basisordner nicht finden kann, dann bleibt die Site angehalten.

Erweitern von Cmdlets für die Arbeit mit einer Serverfarm

Derzeit haben wir zwei Befehle: „get-iissite“ und „add-iissite“. Es fehlen Cmdlets zum Speichern der geänderten Site und zum Löschen der Site. Der Löschbefehl sollte „remove-iissite“ sein, damit er mit den PowerShell-Namenskonventionen konform ist. Der Speicherbefehl heißt „set-iissite“. Für „remove-iissite“ ändern wir den Code für „get-iissite“ und rufen die Methode „Delete()“ für „ManagementObject“ auf.

[Cmdlet(VerbsCommon.Remove, "IISSite", SupportsShouldProcess = true)]
public class RemoveSiteCommand : RemotingCommand
{
    private string name = null;
    [Parameter(
       Position = 0,
       ValueFromPipeline = true,
       ValueFromPipelineByPropertyName = true)]
    [ValidateNotNullOrEmpty]
    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    protected override void EndProcessing()
    {
        base.EndProcessing();

        if (ShouldProcess(string.Format("{0} on server {1}", name, Computer)))
        {
            ManagementObject site = CreateClassInstance(Computer, "Site.Name=\"" + Name + "\"");
            site.Delete();
        }
    }
} //RemoveSiteCommand

Außerdem wurde dem übergeordneten Cmdlet die einfache Methode „CreateClassInstance()“ hinzugefügt. Diese Methode generiert die Objektinstanz, die an den Objektpfad gebunden ist. Eine weitere Änderung besteht darin, dass der Parameter „Name“ jetzt nicht leer sein darf. Andernfalls kann der Benutzer versehentlich alle Sites löschen. Schließlich wurde der Aufruf von „ShouldProcess()“ hinzugefügt, um die Parameter „–whatif“ und „–confirm“ zu aktivieren.

> Remove-IISSite foo -computer test-server -confirm

Confirm
Are you sure you want to perform this action?
Performing operation "Remove-IISSite" on Target "foo on server sergeia-a".
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help
(default is "Y"): <CR>

> get-iissite -computer test-server | ft name,status

Name                                    Status
----                                    ------
Default Web Site                        Started

Wir können zur Übung den letzten Befehl „set-iissite“ implementieren, das Cmdlet „add-iissite“ ändern und „Put()“ für „ManagementObject“ aufrufen.

Skalieren Sie nun die Befehle auf, und passen Sie sie an die Arbeit mit mehreren Servern an. Das ist einfach:

Ändern Sie die Eigenschaft „Computer“ im übergeordneten Befehl, um ein Array von Zeichenfolgen darzustellen:

private string[] computer = { Environment.MachineName };
[Parameter(
   ValueFromPipeline = true,
   ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
public string[] Computer
{
    get { return computer; }
    set { computer = value; }
}

Fügen Sie dann in jedem Cmdlet eine zusätzliche Schleife für dieses Array hinzu, um dieselbe Aktion auf jedem Computer auszuführen. Hier ist ein Beispiel von „get-iissite“:

foreach (string computerName in Computer)
        {
            ManagementObjectCollection sites = CreateClassObject(computerName, "Site").GetInstances();
            foreach (ManagementObject site in sites)
            {
                if (Name != null)
                {
                    string siteName = site.GetPropertyValue("Name") as string;
                    if (siteName.Equals(Name, StringComparison.InvariantCultureIgnoreCase))
                    {
                        WriteObject(ConstructPSSite(computerName, site));
                        break;
                    }
                }
                else 
               {
                    WriteObject(ConstructPSSite(computerName, site));
                }
            }
        }

Jetzt können wir Sites in der gesamten Serverfarm bearbeiten.

> get-iissite -computer test-server,iissb-101,iissb-102 | ft Computer,Name,Status

Computer                   Name                       Status
--------                   ----                       ------
test-server                Default Web Site           Started
iissb-101                  Default Web Site           Started
iissb-101                  Demo                       Started
iissb-102                  Default Web Site           Started

Speichern Sie die Servernamen aus der Farm in einer Textdatei, und verwenden Sie sie als Parameter:

>$("test-server","iissb-101","iissb-102" >farm.txt
>cat farm.txt

test-server 
tissb-101
tissb-102
>get-iissite –computer $(cat farm.txt) | ft Computer,Name,Status

Computer                   Name                       Status
--------                   ----                       ------
test-server                Default Web Site           Started
iissb-101                  Default Web Site           Started
iissb-101                  Demo                       Started
iissb-102                  Default Web Site           Started
>get-iissite –computer $(cat farm.txt) | where {$_.Computer –like "iissb*"} | ft Computer,Name,Status
 
Computer                   Name                       Status
--------                   ----                       ------
iissb-101                  Default Web Site           Started
iissb-101                  Demo                       Started
iissb-102                  Default Web Site           Started

Wir können komplexere Aufgaben mit mehr PowerShell-Code erledigen. Der folgende Code listet Sites auf Servern mit Namen auf. Speichern Sie die Liste ab „iissb“ in der Variablen, und beenden Sie dann alle Sites, die gestartet wurden.

> $sitelist = get-iissite -computer $(cat farm.txt) | where {$_.Computer -like "iissb*"}
> foreach ($site in $sitelist) {
>> if ($site.Status -eq "Started") {$site.Stop()}
>> }
>>
> get-iissite -computer $(cat farm.txt) | ft Computer,Name,Status

Computer                   Name                       Status
--------                   ----                       ------
test-server                Default Web Site           Started
iissb-101                  Default Web Site           Stopped
iissb-101                  Demo                       Stopped
iissb-102                  Default Web Site           Stopped

Die Variable „$sitelist“ speichert die Siteliste, aber dank der dynamischen Eigenschaft „site.Status“ sehen wir den tatsächlichen, nicht gespeicherten Status jedes Objekts.

> $sitelist | ft computer,name,status

Computer                   Name                       Status
--------                   ----                       ------
iissb-101                  Default Web Site           Stopped
iissb-101                  Demo                       Stopped
iissb-102                  Default Web Site           Stopped

Wir können dasselbe tun, ohne Variablen zu verwenden. Starten Sie irgendeine beendete Site auf einem beliebigen Server in der Serverfarm.

> get-iissite -computer (cat farm.txt) | foreach { if ($_.Status -eq "Stopped") { $_.Start() }}
> get-iissite -computer $(cat farm.txt) | ft Computer,Name,Status
 
Computer                   Name                       Status
--------                   ----                       ------
test-server                Default Web Site           Started
iissb-101                  Default Web Site           Started
iissb-101                  Demo                       Started
iissb-102                  Default Web Site           Started

In der zugehörigen Quelldatei „iisdemocmd.cs“ finden Sie weitere Befehle zum Bearbeiten virtueller Verzeichnisse und einiger Eigenschaften in den Konfigurationsabschnitten.

Zusammenfassung

Wie Sie sehen, können wir mit nur drei Befehlen die meisten Anforderungen bei der Verwaltung von IIS-Sites abdecken. In Kombination mit der Flexibilität und Vielseitigkeit der Shellsprache eröffnet jeder Befehl viele Möglichkeiten. Gleichzeitig ist das Schreiben eines neuen Befehls nicht viel komplizierter als die Implementierung ähnlicher Skripts in VBScript oder Jscript.

Das IIS-Team plant, die vollständige Unterstützung von PowerShell in IIS 7.0 und höher hinzuzufügen. Dazu gehört die Implementierung eines Navigationsanbieters, eines Eigenschaftenanbieters und aller anderen Funktionen, die für die Arbeit mit allen Aspekten der Verwaltung erforderlich sind. Auf https://www.iis.net/ und der PowerShell-Website finden Sie Verbesserungen und Ankündigungen.