Compartir a través de


Escritura de commandlets de PowerShell para IIS 7.0

de Sergei Antonov

Introducción

Con PowerShell enviado, los administradores de IIS obtienen una nueva herramienta que deben usar. El artículo siguiente se centra en tareas de administración para IIS 7.0 y versiones posteriores; sin embargo, PowerShell se puede usar para los servidores IIS 6.0 existentes.

Este artículo se centra en la administración del servidor IIS remoto mediante PowerShell en el equipo cliente. En el momento de escribir este documento, esto solo es posible si usa el proveedor de WMI que el equipo de IIS envió con Vista y Windows Server® 2008. En este caso, no es necesario tener nada relacionado con IIS en el equipo cliente: WMI proporciona la conexión a la configuración real disponible en el servidor remoto.

Nota:

También puede usar Microsoft.Web.Administration en PowerShell para realizar funciones de administración. Sin embargo, este artículo no se centra en esa técnica.

El equipo de PowerShell creó un comando especial que se va a usar para el acceso a objetos de WMI: get-wmiobject. Normalmente devuelve un objeto , que se crea dentro de PowerShell y expone propiedades y métodos de WMI, en lugar del objeto de código administrado habitual que se devuelve para las clases normales.

Este objeto sintético expone los metadatos definidos en el espacio de nombres WMI, no aquellos de System.Management.ManagementBaseObject usados como base. Esto proporciona al usuario la vista del espacio de nombres expuesta por el proveedor de WMI y que refleja el modelo de administración de la entidad configurada.

Desafortunadamente, este comando no funciona con espacios de nombres IIS. Para PowerShell versión 1.0, get-wmiobject solo admite el nivel de autenticación predeterminado para la conexión DCOM remota. Esto no es suficiente ni para IIS 6.0 ni para IIS 7.0 y versiones posteriores. Cuando los usuarios configuran IIS, puede ser necesario enviar contraseñas y otros datos confidenciales a través de la conexión de red para editarlos y almacenarlos en la configuración. Para admitirlo, los proveedores de IIS requieren un nivel de autenticación "Privacidad de paquete". No hay ninguna manera de proporcionar este requisito al cmdlet get-wmiobject.

Con esta limitación, tenemos dos opciones:

  • Use PowerShell como interfaz de scripting genérica para el espacio de nombres System.Management. Use PowerShell también para hacer lo siguiente: escribir código de script que configure la conexión con el espacio de nombres WMI remoto; y para recuperar y guardar datos de administración mediante objetos System.Management, a los que se accede a través de PowerShell. Es como la programación de C#, solo en el lenguaje PowerShell. Normalmente esto funciona bien pero, en el caso de WMI, PowerShell aplica un adaptador especial que convierte automáticamente todos los objetos System.Management en objetos de PowerShell sintéticos que exponen el espacio de nombres WMI como API principal. Por este motivo, debemos superar los cambios del adaptador y escribir código adicional para acceder a la subestructura System.Management "nativa", lo que convierte rápidamente esta programación en un ejercicio innecesariamente complicado.

Por lo tanto, exploramos la otra opción:

  • Escriba cmdlets de PowerShell en C# y acceda a la funcionalidad necesaria en código de C#. En este caso, se eligen las API adecuadas para las entidades configuradas. Usamos WMI para acceder al servidor. En comparación con el mismo código de PowerShell, C# es mucho más eficaz, ya que no es necesario analizarlo e interpretarlo cada vez.

Cmdlet de PowerShell

Para empezar a escribir cmdlets, necesita un equipo cliente instalado con PowerShell. También debe instalar el SDK de PowerShell o simplemente copiar archivos DLL de referencia en la carpeta de trabajo mediante el truco publicado por Jeffrey Snover en el blog del equipo de PowerShell. Asegúrese de que tiene la conexión DCOM en el servidor. La manera más fácil de confirmarlo es iniciar la utilidad wbemtest, que está disponible en cada plataforma de Windows y probar la conexión.

  1. Inicie wbemtest.
  2. Haga clic en Conectar.
  3. Escriba parámetros de conexión:
    • Reemplace "root\default" por \<computer>\root\webadministration, donde "<computer>" debe ser el nombre del servidor.
    • Escriba las credenciales de la cuenta con derechos de administrador en el servidor.
    • Seleccione "Privacidad de paquete" en el grupo de nivel de autenticación.
  4. Haga clic en Conectar. WMI en el equipo cliente se conecta al servicio WMI en el equipo servidor. Si no es accesible, aparecerá un cuadro de diálogo de mensaje de error.
  5. Realice alguna acción sencilla que involucre al proveedor de WMI en el cuadro del servidor para confirmar que funciona. Realice una enumeración de tipos:
    • Haga clic en el botón "Instancias de enumeración" y escriba "site" para el nombre de clase.
    • Cuando funciona, en el cuadro de diálogo resultante se muestra una lista de todos los sitios disponibles en el servidor.

El cmdlet de PowerShell es simplemente un ensamblado de código administrado implementado siguiendo reglas formales documentadas en el SDK de PowerShell. Búsquelas en línea.

Antes de escribir cualquier código, resulta útil tener un plan.

En primer lugar, implemente un cmdlet que enumere todos los sitios de IIS en el servidor remoto. Este cmdlet devuelve una matriz de objetos de sitio, que representan el elemento de configuración de IIS con propiedades, definidas para el sitio. Agregaremos algunas propiedades adicionales que son útiles en ese objeto.

Queremos que el cmdlet tenga un aspecto similar al siguiente:

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

Si no se pasa la credencial al cmdlet mediante programación, PowerShell genera un cuadro de diálogo en el que se solicitan el nombre de usuario y la contraseña para el servidor remoto.

Para obtener el objeto de sitio del equipo remoto, debemos proporcionar los siguientes parámetros de nuestro cmdlet:

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

Todos estos parámetros son propiedades públicas en la clase del cmdlet, decoradas por el atributo Parameter.

Implemente el primer cmdlet. Puesto que get-iissite no es nuestro último comando, es mejor hacer dos cosas: separar el código responsable de la conexión al servidor en la clase principal RemotingCommand; y heredar el cmdlet de esa clase.

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()
             );
        }
    }

La clase RemotingCommand incluye parámetros y métodos necesarios para la conexión.

  • GetScope() devuelve el objeto System.Management que tiene toda la información necesaria para la conexión al espacio de nombres remoto. Examine la propiedad connection.Authentication. Se inicializa en AuthenticationLevel.PacketPrivacy. Se trata de un requisito obligatorio. De lo contrario, WMI rechazará la conexión.
  • El método CreateClassObject() es un método de utilidad que usa el ámbito de conexión para crear una clase especificada basada en el espacio de nombres WMI remoto.
  • El método EndProcessing() es el método estándar que cada clase del cmdlet debe implementar. Se le llama desde PowerShell cuando se procesa nuestro cmdlet. La implementación de EndProcessing() intenta rellenar la propiedad de credencial, si está vacía. En el primer intento, obtiene la credencial de la variable externa IISCredential (solo para mayor comodidad).

Cuando se encuentra en una sesión de PowerShell, es posible que el usuario quiera colocar el nombre de usuario y la contraseña en esta variable y usarlos varias veces en varios comandos. Si esta variable no está definida o contiene un tipo de objeto inadecuado, el código obtiene las credenciales del usuario o proceso actual. Funciona cuando el usuario ejecuta este comando localmente en el servidor mediante la cuenta de administrador. En este caso, no es necesario escribir ninguna credencial en absoluto. Para cada bucle de este código, hay un truco conocido para convertir la contraseña de la cadena en SecureString.

Ahora implementamos 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

En el primer corte, el comando devuelve solo nombres de sitio. Si el usuario quiere algún sitio específico, debe proporcionar el nombre de este sitio al comando; de lo contrario, se devolverán todos los sitios de ese equipo concreto.

Para finalizar el comando, debe agregar la implementación de la clase, heredada de PSSnapin. Esta clase se usa para registrar nuestros comandos. No tiene nada específico de IIS; consulte el código completo en el archivo de origen IISDemoCmd.cs.

Compile el cmdlet ahora y vea cómo funciona. Podría hacerlo desde Visual Studio, pero es lo suficientemente sencillo para compilarlo desde la línea de comandos. Supongamos que ha colocado archivos DLL de referencia de PowerShell en una carpeta c:\sdk. La siguiente línea de comandos compila el cmdlet en IISDemoCmd.dll y lo coloca en la misma carpeta, donde se encuentra el archivo de origen.

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

Ahora debe registrar el comando y agregarlo a PowerShell. Este procedimiento se describe en Referencia de programación de PowerShell. Inicie PowerShell y ejecute los siguientes comandos desde la misma carpeta donde compiló el archivo DLL del cmdlet.

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

Esto agrega el cmdlet a la instancia en ejecución de PowerShell. Guarde esas líneas de comandos en un archivo de script. Los usará de nuevo a medida que siga trabajando en los cmdlets. Encontrará este script en el archivo demo_install.ps1.

Compruebe si el comando está disponible:

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

Ahora, pruébelo. Supongamos que se conecta al equipo test_server, mediante la cuenta de administrador local.

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

Esta línea de comandos recibe el objeto de credencial del cmdlet get-credential, que interactuará con el usuario para obtener la contraseña. También es posible generar la credencial mediante programación, pero debe escribir la contraseña en el script, que no es seguro en absoluto.

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

Este comando almacena la credencial en la variable global $iiscredential y el cmdlet la usará automáticamente. Sin embargo, en situaciones reales, es mejor almacenar la credencial en una variable mediante el comando get-credential: $global:iiscredential = administrador get-credential.

Ahora ese comando de nuevo.

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

Toda la infraestructura está dispuesta. Ahora, para volver al comando y agregar el resto de los datos del sitio.

Adición de datos de configuración al sitio

Tenemos que convertir el objeto de ManagementBaseObject a PSObject y devolverlo a PowerShell. PSObject es un contenedor de forma libre que se puede rellenar con diferentes tipos de datos. Usaremos el tipo PSNoteProperty. Mantenga el código del cmdlet limpio y agregue una nueva clase responsable de la conversión.

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;
    }
}

Este código recurre a propiedades complejas y agrega propiedades simples como PSNoteProperty al PSObject resultante. También se agrega un método dedicado que se ocupará de la conversión en el cmdlet out. Este método convierte todos los datos WMI y agrega dos propiedades más: el nombre del equipo y la credencial que se usó para obtener la conexión a este equipo. Esto ayuda a distinguir cada sitio de otros objetos en la sesión de PowerShell.

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;
}

Reemplace el nombre del sitio que devolvimos a PowerShell por un objeto completo. El método EndProcessing() del cmdlet ahora tiene el siguiente aspecto:

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));
        }
    }
}

Cuando repitamos la compilación y el registro, y volvamos a ejecutar el comando, veremos más datos sobre el sitio:

> 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

Si lo compara con el esquema WMI del sitio, verá que todos los datos están disponibles ahora; además, tenemos propiedades adicionales que agregamos en el cmdlet. Todas las propiedades son accesibles desde PowerShell a través de la notación "de puntos".

> $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

Hemos realizado un gran progreso, pero no lo bastante bueno. El sitio WMI también tiene métodos, así que intente agregarlos también. Esto es fácil de hacer en PowerShell: se asigna un nombre al método y se indica a PowerShell dónde se encuentra el código. Agregaremos métodos del tipo PSCodeMethod. Para mantener el código de los métodos, agregamos la clase SiteMethods.

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[] { });
    }
}

Como ve, este código crea un objeto WMI para el sitio y llama a métodos WMI en este objeto. Este código usa dos propiedades adicionales que agregamos al sitio. Con esta clase, podemos ampliar el método ConstructPSSite. También debe agregarse una referencia al espacio de nombres System.Reflection.

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;
}

Además de los métodos agregados, hay una propiedad dinámica: "Status". Se comporta del mismo modo que las propiedades de las clases de C#; es una función a la que se llama cuando PowerShell necesita su valor. El código es muy sencillo, ya que hacemos referencia a métodos del mismo ensamblado que nuestro cmdlet. Nada impide cargar ningún otro ensamblado y obtener la información sobre los métodos de sus clases. Si esos métodos tienen la firma correcta, PowerShell la usa de la misma manera.

El objeto ahora tiene el aspecto siguiente:

>$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

Con la capacidad de agregar métodos y propiedades dinámicas a los objetos, podemos sintetizar lo que necesitamos para cualquier situación. Además de los métodos y propiedades agregados en el cmdlet, podemos agregar más en el script, sin necesidad de usar código de C#.

También es posible cargar la definición del objeto desde XML. Un buen candidato para propiedades adicionales son los datos expuestos desde IIS a través de contadores de rendimiento relacionados con el sitio; por ejemplo, el recuento total de solicitudes procesadas. Estos datos son fácilmente accesibles directamente desde el código administrado: no es necesario usar WMI.

Cómo llamar a un cmdlet desde otro cmdlet

La obtención de objetos de sitio es importante, pero necesitamos más, como escribir un comando para agregar un nuevo sitio. Para crear sitios, podemos usar el método abstracto Create() definido en la clase Site del espacio de nombres WebAdministration de WMI. El cmdlet es similar al siguiente:

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

Tenemos los mismos parámetros tal como se define en el método Create. Además, el comando debe admitir los modificadores –whatif y –passthru. El primero muestra el resultado de la ejecución del comando, pero no realiza ningún cambio; el segundo indica al comando que exporte el resultado en la canalización. Se recomienda encarecidamente usar estos dos modificadores en comandos "destructivos". Para admitir el cmdlet –whatif, el atributo SupportsShouldProcess debe decorar la clase = true.

A continuación se muestra parte del código (busque todo el código en el 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);
            }
        }
    }
}

Este código usa un nuevo método en la clase ObjectConverter para generar una matriz de enlaces. El método ToManagementObject() convierte los parámetros de entrada que pueden ser PSObject o Hashtable en la instancia de la clase ManagementBaseObject. Dado que la llamada a Create podría fallar con parámetros perfectamente correctos si el sitio con esos parámetros ya está disponible, llamamos a este método en try/catch.

Por último, si el cmdlet comprueba si el usuario especificado –passthru está presente, llama a PowerShell para ejecutar un fragmento de script que devuelve este nuevo sitio. En este script, llamamos a nuestro comando "get-iissite" y reutilizamos los parámetros pasados al comando actual. InvokeScript coloca el resultado en la canalización como se ha solicitado, por lo que no es necesario hacer nada más. Este es un ejemplo de cómo podemos hacer una "devolución de llamada" a PowerShell, pasando líneas de comandos con formato estática o dinámicamente. Por supuesto, es posible escribirlo como código de C#, pero requiere cortar y pegar grandes partes de GetSiteCommand, o reorganizar y refactorizar el espacio de nombres.

Al principio del método EndProcessing() vemos la llamada a ShouldProcess(). Así es como se admite el modificador –whatif. Cuando el usuario pasa este modificador, este método imprime el texto que se le pasa como parámetro y el valor devuelto es false. Todas las acciones que pueden cambiar el entorno deben realizarse cuando esta llamada devuelva true. PowerShell tiene otros modificadores que pueden interactuar con el usuario y pedir confirmación antes de realizar cualquier acción. ShouldProcess() se devuelve como resultado de esta confirmación.

Pruebe el nuevo comando con lo siguiente:

> 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".

Así es como funciona –whatif. Obtenemos un mensaje bastante críptico sobre lo que ocurre al ejecutarse este comando. Para que sea más claro, debemos formatearlo correctamente. Enlaces de parámetros se escribe en la línea de comandos como una tabla hash y se pasa al cmdlet como Hashtable encapsulado en PSObject. Para generar texto significativo a partir de él, debemos agregar código más inteligente: la sintaxis ToString() predeterminada simplemente devuelve el nombre de clase.

Inserte este bloque de texto en lugar de la línea ShouldProcess():

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)))

Una vez compilado y ejecutado el cmdlet, vemos el resultado siguiente:

> 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".

Esto es mucho más comprensible. A partir de este nuevo código, también está claro por qué debemos procesar Hashtable en el método ToManagementObject(): es un tipo común en PowerShell para pasar parámetros estructurados.

A continuación, ejecute el comando.

> 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

El primer comando creó el sitio en el servidor remoto y, a continuación, lo recuperó y pasó a la canalización. Para garantizar que se hiciera correctamente, conseguimos una lista de los sitios y, de hecho, el nuevo sitio está disponible. De manera predeterminada, el servidor intentará iniciar el sitio, a menos que agreguemos el parámetro –AutoStart false. Si hay algún problema en los parámetros (por ejemplo, el servidor no encuentra la carpeta principal), el sitio permanecerá detenido.

Ampliación de cmdlets para trabajar con una granja de servidores

Por ahora, tenemos dos comandos: get-iissite y add-iissite. Faltan cmdlets para guardar el sitio modificado y eliminar el sitio. El comando de eliminación debe ser remove-iissite, para mantenerlo compatible con los estándares de nomenclatura de PowerShell. El comando Guardar tendrá un nombre set-iissite. Para remove-iissite, modificamos el código get-iissite y llamamos al método Delete() en ManagementObject.

[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

También hemos agregado un método simple CreateClassInstance() al cmdlet primario. Este método genera la instancia del objeto enlazada a la ruta de acceso del objeto. Otro cambio es que el parámetro Name ahora no puede estar vacío; de lo contrario, el usuario puede eliminar todos los sitios por error. Por último, hemos agregado la llamada a ShouldProcess() para habilitar los modificadores –whatif y –confirm.

> 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

Podemos implementar el último comando set-iissite como ejercicio, modificando el cmdlet add-iissite y llamando a Put() en ManagementObject.

Ahora escale horizontalmente los comandos y adáptelos para trabajar con varios servidores. Es fácil:

Cambie la propiedad Computer en el comando primario para representar una matriz de cadenas:

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

A continuación, agregue un bucle adicional sobre esta matriz en cada cmdlet para realizar la misma acción en cada equipo. Este es un ejemplo de 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));
                }
            }
        }

Ahora podemos manipular sitios en toda la granja de servidores.

> 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

Guarde los nombres de servidor de la granja en un archivo de texto y úselos como parámetro:

>$("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

Podemos hacer cosas más avanzadas con más lenguaje PowerShell. El código siguiente enumera sitios en servidores con nombres. A partir de "iissb", almacene la lista en la variable y, a continuación, detenga todos los sitios que se inicien.

> $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

La variable $sitelist mantiene la lista de sitios, pero gracias a la naturaleza dinámica de la propiedad site.Status, vemos el estado real, no almacenado, de cada objeto.

> $sitelist | ft computer,name,status

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

Podemos hacer lo mismo sin usar ninguna variable. Inicie cualquier sitio detenido en cualquier servidor de la granja de servidores.

> 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

En el archivo de origen de acompañamiento iisdemocmd.cs, encontrará más comandos para manipular directorios virtuales y algunas propiedades en las secciones de configuración.

Conclusión

Como podemos ver, tener solo tres comandos nos permite cubrir la mayoría de las necesidades en la administración de sitios de IIS. En combinación con la flexibilidad y riqueza del lenguaje de shell, cada comando agrega una gran cantidad de funcionalidades. Al mismo tiempo, escribir un nuevo comando no es mucho más complicado que implementar un script similar en VBScript o Jscript.

El equipo de IIS tiene previsto agregar compatibilidad completa con PowerShell en IIS 7.0 y versiones posteriores. Esto incluye la implementación de un proveedor de navegación, un proveedor de propiedades y todas las demás funciones necesarias para trabajar con todos los aspectos de la administración. Siga el progreso de estas próximas mejoras y busque el anuncio en https://www.iis.net/ y en el sitio de PowerShell.