Construyendo el HTML
Voy a abandonar el CmdLet nativo de ConvertTo-HTML que he discutido hasta ahora, En lugar de eso, voy a pedirle que utilice el módulo EnhancedHTML2 que viene con este e-Book. Tenga en cuenta que, a partir de octubre de 2013, se trata de una nueva versión del módulo - es más sencillo que el módulo EnhancedHTML introducido con la edición original de este libro.
Comencemos con el script que utiliza el módulo. Se incluye con este libro como EnhancedHTML2-Demo.ps1, por lo que aquí voy a pegarlo aquí y luego agregare las explicaciones sobre lo que hace cada bit. Tenga en cuenta que no puedo controlar cómo se ve el código en un e-Reader, por lo que es probable que parezca un poco desordenado.
1 #requires -module EnhancedHTML2
2 <#
3 .SYNOPSIS
4 Generates an HTML-based system report for one or more com\
5 puters.
6 Each computer specified will result in a separate HTML fi\
7 le;
8 specify the -Path as a folder where you want the files wr\
9 itten.
10 Note that existing files will be overwritten.
11
12 .PARAMETER ComputerName
13 One or more computer names or IP addresses to query.
14
15 .PARAMETER Path
16 The path of the folder where the files should be written.
17
18 .PARAMETER CssPath
19 The path and filename of the CSS template to use.
20
21 .EXAMPLE
22 .\New-HTMLSystemReport -ComputerName ONE,TWO `
23 -Path C:\Reports\
24 #>
25 [CmdletBinding()]
26 param(
27 [Parameter(Mandatory=$True,
28 ValueFromPipeline=$True,
29 ValueFromPipelineByPropertyName=$True)]
30 [string[]]$ComputerName,
31
32 [Parameter(Mandatory=$True)]
33 [string]$Path
34 )
La sección anterior nos dice que se trata de un “script avanzado”, lo que significa que utiliza el enlace de CmdLet de PowerShell. Puede especificar uno o más nombres de equipo para los que se genera el informe, y debe especificar una ruta de acceso de carpeta (no un nombre de archivo) para almacenar los reportes finales.
1 BEGIN {
2 Remove-Module EnhancedHTML2
3 Import-Module EnhancedHTML2
4 }
El bloque BEGIN podría ser eliminado dependiendo de la versión de PowerShell que esté utilizando. Utilizo esta demostración para probar el módulo, así que es importante que descargue cualquier versión antigua de la memoria (si ha cargado el módulo anteriormente) y vuelva a cargar la versión revisada. De hecho, PowerShell v3 y posterior no requerirá la importación si el módulo está correctamente ubicado en \Documents\WindowsPowerShell\Modules\EnhancedHTML2.
1 PROCESS {
2
3 $style = @"
4 <style>
5 body {
6 color:#333333;
7 font-family:Calibri,Tahoma;
8 font-size: 10pt;
9 }
10
11 h1 {
12 text-align:center;
13 }
14
15 h2 {
16 border-top:1px solid #666666;
17 }
18
19 th {
20 font-weight:bold;
21 color:#eeeeee;
22 background-color:#333333;
23 cursor:pointer;
24 }
25
26 .odd { background-color:#ffffff; }
27
28 .even { background-color:#dddddd; }
29
30 .paginate_enabled_next, .paginate_enabled_previous {
31 cursor:pointer;
32 border:1px solid #222222;
33 background-color:#dddddd;
34 padding:2px;
35 margin:4px;
36 border-radius:2px;
37 }
38
39 .paginate_disabled_previous, .paginate_disabled_next {
40 color:#666666;
41 cursor:pointer;
42 background-color:#dddddd;
43 padding:2px;
44 margin:4px;
45 border-radius:2px;
46 }
47
48 .dataTables_info { margin-bottom:4px; }
49
50 .sectionheader { cursor:pointer; }
51
52 .sectionheader:hover { color:red; }
53
54 .grid { width:100% }
55
56 .red {
57 color:red;
58 font-weight:bold;
59 }
60 </style>
61 "@
Eso se llama hoja de estilos en cascada, o CSS. Hay algunas cosas interesantes para sacar destacar:
He colocado toda la sección <style></ style> en una cadena here-string de PowerShell, y almacenado en la variable $style. Hará que sea fácil referirse a esto en adelante.
Tenga en cuenta que he definido el estilo de varias etiquetas HTML, como H1, H2, BODY y TH. Esas definiciones de estilo listan el nombre de la etiqueta sin un signo anterior de período o hash. Se definen los elementos de estilo que interesan, como el tamaño de la fuente, la alineación del texto, etc. Etiquetas como H1 y H2 ya tienen estilos predefinidos establecidos por su navegador, como su tamaño de fuente. Cualquier cosa que ponga en el CSS reemplazará los valores predeterminados del navegador.
Los estilos también heredan. Todo el cuerpo de la página HTML está contenido dentro de las etiquetas <BODY></ BODY>, por lo que cualquier cosa que asigne a la etiqueta BODY en CSS también se aplicará a todo lo que contenga la página. Mi cuerpo establece una familia de fuentes y un color de fuente. Las etiquetas H1 y H2 usarán la misma fuente y color.
También verá las definiciones de estilo precedidas por un punto. Esos se llaman estilos de clase. Son clase de plantillas reutilizables del estilo que se pueden aplicar a cualquier elemento dentro de la página. Los “.paginate” son realmente utilizados por el JavaScript que uso para crear tablas dinámicas. No me gustó la forma en que los botones Prev / Next se veían fuera de la caja, así que modifiqué mi CSS para aplicar estilos diferentes.
Preste mucha atención a .odd, .even, y .red en el CSS. Vera que los utilizo poco a poco.
1 function Get-InfoOS {
2 [CmdletBinding()]
3 param(
4 [Parameter(Mandatory=$True)][string]$ComputerName
5 )
6 $os = Get-WmiObject -class Win32_OperatingSystem -Com\
7 puterName $ComputerName
8 $props = @{'OSVersion'=$os.version
9 'SPVersion'=$os.servicepackmajorversion;
10 'OSBuild'=$os.buildnumber}
11 New-Object -TypeName PSObject -Property $props
12 }
13
14 function Get-InfoCompSystem {
15 [CmdletBinding()]
16 param(
17 [Parameter(Mandatory=$True)][string]$ComputerName
18 )
19 $cs = Get-WmiObject -class Win32_ComputerSystem -Comp\
20 uterName $ComputerName
21 $props = @{'Model'=$cs.model;
22 'Manufacturer'=$cs.manufacturer;
23 'RAM (GB)'="{0:N2}" -f ($cs.totalphysicalm\
24 emory / 1GB);
25 'Sockets'=$cs.numberofprocessors;
26 'Cores'=$cs.numberoflogicalprocessors}
27 New-Object -TypeName PSObject -Property $props
28 }
29
30 function Get-InfoBadService {
31 [CmdletBinding()]
32 param(
33 [Parameter(Mandatory=$True)][string]$ComputerName
34 )
35 $svcs = Get-WmiObject -class Win32_Service -ComputerN\
36 ame $ComputerName `
37 -Filter "StartMode='Auto' AND State<>'Running'"
38 foreach ($svc in $svcs) {
39 $props = @{'ServiceName'=$svc.name;
40 'LogonAccount'=$svc.startname;
41 'DisplayName'=$svc.displayname}
42 New-Object -TypeName PSObject -Property $props
43 }
44 }
45
46 function Get-InfoProc {
47 [CmdletBinding()]
48 param(
49 [Parameter(Mandatory=$True)][string]$ComputerName
50 )
51 $procs = Get-WmiObject -class Win32_Process -Computer\
52 Name $ComputerName
53 foreach ($proc in $procs) {
54 $props = @{'ProcName'=$proc.name;
55 'Executable'=$proc.ExecutablePath}
56 New-Object -TypeName PSObject -Property $props
57 }
58 }
59
60 function Get-InfoNIC {
61 [CmdletBinding()]
62 param(
63 [Parameter(Mandatory=$True)][string]$ComputerName
64 )
65 $nics = Get-WmiObject -class Win32_NetworkAdapter -Co\
66 mputerName $ComputerName `
67 -Filter "PhysicalAdapter=True"
68 foreach ($nic in $nics) {
69 $props = @{'NICName'=$nic.servicename;
70 'Speed'=$nic.speed / 1MB -as [int];
71 'Manufacturer'=$nic.manufacturer;
72 'MACAddress'=$nic.macaddress}
73 New-Object -TypeName PSObject -Property $props
74 }
75 }
76
77 function Get-InfoDisk {
78 [CmdletBinding()]
79 param(
80 [Parameter(Mandatory=$True)][string]$ComputerName
81 )
82 $drives = Get-WmiObject -class Win32_LogicalDisk -Com\
83 puterName $ComputerName `
84 -Filter "DriveType=3"
85 foreach ($drive in $drives) {
86 $props = @{'Drive'=$drive.DeviceID;
87 'Size'=$drive.size / 1GB -as [int];
88 'Free'="{0:N2}" -f ($drive.freespace /\
89 1GB);
90 'FreePct'=$drive.freespace / $drive.si\
91 ze * 100 -as [int]}
92 New-Object -TypeName PSObject -Property $props
93 }
94 }
Las seis funciones anteriores no hacen otra cosa que recuperar datos de una sola computadora (observe que su parámetro -ComputerName se define como [string], aceptando un valor, en lugar de [string[]] que aceptaría múltiples). Si no logra entender cómo funciona esto… es probable que tenga que dar un paso atrás!
Para propósitos de formato, usted está viendo que se utiliza el carácter (back tick) (como en –ComputerName y $ComputerName). En PowerShell este carácter funciona como una especie de continuación de línea. Lo señalo porque puede ser fácil perderlo de vista.
1 foreach ($computer in $computername) {
2 try {
3 $everything_ok = $true
4 Write-Verbose "Checking connectivity to $computer"
5 Get-WmiObject -class Win32_BIOS -ComputerName $Co\
6 mputer -EA Stop | Out-Null
7 } catch {
8 Write-Warning "$computer failed"
9 $everything_ok = $false
10 }
Lo anterior es el inicio de mi script de demostración. Se están tomando los nombres de equipo que se pasaron al parámetro -ComputerName, procesándolos uno a la vez. Luego se hace una llamada a Get-WmiObject como una prueba - si esto falla, no quiero hacer nada con el nombre del equipo en absoluto. El resto de la secuencia de comandos sólo se ejecuta si esa llamada WMI tiene éxito.
1 if ($everything_ok) {
2 $filepath = Join-Path -Path $Path -ChildPath "$co\
3 mputer.html"
Recuerde que el otro parámetro de este script es -Path. Estoy utilizando Join-Path para combinar $Path con un nombre de archivo. Join-Path garantiza el número correcto de barras inversas, de modo que si -Path es “C:” o “C:” obtendré una ruta de archivo válida. El nombre de archivo será el nombre del equipo actual, seguido de la extensión .html.
1 $params = @{'As'='List';
2 'PreContent'='<h2>OS</h2>'}
3 $html_os = Get-InfoOS -ComputerName $computer |
4 ConvertTo-EnhancedHTMLFragment @params
Aquí está mi primer uso del módulo EnhancedHTML2: Con ConvertTo-EnhancedHTMLFragment. Observe lo que estoy haciendo:
- Estoy usando un hashtable para definir los parámetros del comando, incluyendo ambos -As List y -PreContent ‘
<H2>OS</H2>’ como parámetros y sus valores. Esto especifica una salida de estilo de lista (frente a una tabla), precedida por el encabezado “OS” en el estilo H2. Vuelva a mirar el CSS y verá que he aplicado un borde superior a todo el elemento<H2>, lo que ayudará a separar visualmente las secciones de mi informe. - Estoy ejecutando mi comando Get-InfoOS, pasando el nombre del equipo actual. La salida se canaliza a…
- ConvertTo-EnhancedHTMLFragment, ConvertTo-EnhancedHTMLFragment, donde se encuentra mi hashtable de parámetros. El resultado será una gran cadena de HTML, que se almacenará en $html_os.
1 $params = @{'As'='List';
2 'PreContent'='<h2>Computer System</h2\
3 >'}
4 $html_cs = Get-InfoCompSystem -ComputerName $comp\
5 uter |
6 ConvertTo-EnhancedHTMLFragment @params
Ese es un ejemplo similar, para la segunda sección de mi informe..
1 $params = @{'As'='Table';
2 'PreContent'='<h2>♦ Local Disks\
3 </h2>';
4 'EvenRowCssClass'='even';
5 'OddRowCssClass'='odd';
6 'MakeTableDynamic'=$true;
7 'TableCssClass'='grid';
8 'Properties'='Drive',
9 @{n='Size(GB)';e={$_.Size}},
10 @{n='Free(GB)';e={$_.Free};css={if ($_.Fre\
11 ePct -lt 80) { 'red' }}},
12 @{n='Free(%)';e={$_.FreePct};css={if ($_.F\
13 reeePct -lt 80) { 'red' }}}}
14
15 $html_dr = Get-InfoDisk -ComputerName $computer |
16 ConvertTo-EnhancedHTMLFragment @params
OK, ese es un ejemplo más complejo. Echemos un vistazo a los parámetros que estoy pasando a ConvertTo-EnhancedHTMLFragment:
- Como se está produciendo una tabla en lugar de una lista, la salida será en un diseño de tabla columnar (algo como lo que produciría Format-Table, pero en HTML).
- Para mi sección de encabezado, he añadido un símbolo de diamante utilizando la entidad HTML ♦ Creo que se ve bien. Eso es todo.
- Puesto que esto será una tabla, puedo especificar -EvenRowCssClass y -OddRowCssClass. Le doy los valores “even” y “odd”, que son las dos clases (.even y .odd) que definí en mi CSS. De esta forma estoy creando el vínculo entre las filas de la tabla y mi CSS. Cualquier fila de la tabla “etiquetada” con la clase “odd” heredará el formato de “.odd” de mi CSS. No se debe incluir el punto al especificar los nombres de clase con estos parámetros. Sólo en el CSS se coloca el punto delante del nombre de la clase.
-
-MakeTableDynamicse establece en $True, para que se aplique el JavaScript necesario y convertir la salida en una tabla que se pueda ordenar y paginar. Esto requerirá que el HTML final se vincule al archivo JavaScript necesario, pero cubriremos este punto cuando lleguemos allí. -
-TableCssClasses opcional, pero lo estoy usando para asignar la clase “grid”. Una vez más, si observa el CSS, podrá observar que definí un estilo para “.grid”, por lo que esta tabla heredará esas instrucciones de estilo. - El último es el parámetro
-Properties. Funciona muy parecido a los parámetros-PropertiesdeSelect-ObjectyFormat-Table. El parámetro acepta una lista de propiedades separada por comas. El primero, Drive, ya está siendo producido porGet-InfoDisk. Los siguientes tres son especiales: son hashtables, creando columnas personalizadas como loa haría Format-Tabl. Dentro del hashtable, usted puede utilizar las siguientes claves:- n (o name, o l, o label) especifica el encabezado de columna. Estoy usando “Size(GB),” “Free(GB)”, y “Free(%)” como encabezados de columna.
- e (o expression) es un bloque de secuencia de comandos, que define lo que contendrá la celda de la tabla. Dentro de ella, puede utilizar $_ para referirse al objeto de entrada. En este ejemplo, el objeto canalizado proviene de
Get-InfoDisk, por lo que me refiero a las propiedades Size, Free y FreePct del objeto. - css (o cssClass) es también un bloque de secuencia de comandos. Mientras que el resto de las claves funcionan igual que lo hacen con Select-Object o Format-Table, css (o cssClass) es exclusivo de ConvertTo-EnhancedHTMLFragment. Acepta un bloque de secuencia de comandos, que se espera que produzca una cadena, o nada. En este caso, estoy comprobando para ver si la propiedad FreePct del objeto en la canalización es menor que 80 o no. Si es así, la salida será la cadena “red”. Esta cadena se agregará como una clase CSS de la celda en la tabla. Recuerde que en mi CSS definí la clase “.red” y aquí es donde adjunto esa clase a las celdas de la tabla.
- Como una nota aparte, me doy cuenta de que es tonto establecer un color rojo cuando el porcentaje libre de disco es inferior al 80%. Se trata solo de un ejemplo para jugar. Podría fácilmente tener una fórmula más compleja, como if ($_.FreePct -lt 20) { ‘red’ } elseif ($_.FreePct -lt 40) { ‘yellow’ } else { ‘green’ } y entonces habría definido las clases “.red”, “.yellow” y “.green” en el CSS.
1 $params = @{'As'='Table';
2 'PreContent'='<h2>♦ Proce\
3 sses</h2>';
4 'MakeTableDynamic'=$true;
5 'TableCssClass'='grid'}
6 $html_pr = Get-InfoProc -ComputerName $computer |
7 ConvertTo-EnhancedHTMLFragm\
8 ent @params
9
10 $params = @{'As'='Table';
11 'PreContent'='<h2>♦ Servi\
12 ces to Check</h2>';
13 'EvenRowCssClass'='even';
14 'OddRowCssClass'='odd';
15 'MakeHiddenSection'=$true;
16 'TableCssClass'='grid'}
17
18 $html_sv = Get-InfoBadService -ComputerName $computer |
19 ConvertTo-EnhancedHTMLFrag\
20 ment @params
Más de lo mismo en los dos ejemplos anteriores, con sólo un nuevo parámetro: -MakeHiddenSection. Esto hará que la sección del informe se colapse de forma predeterminada, mostrando sólo la cadena -PreContent. Al hacer clic en la cadena, se expandirá y contraerá la sección del informe.
De regreso en mi CSS, observe que para la clase .sectionHeader, establezco el cursor en un icono de puntero, e hice que el color del texto de la sección fuera rojo cuando el ratón pasa sobre él. Esto ayuda a comprender al usuario que se puede hacer clic en el encabezado de la sección. El módulo EnhancedHTML2 siempre agrega la clase CSS “sectionheader” al -PreContent, por lo que al definir “.sectionheader” en su CSS, puede seguir diseñando los encabezados de sección.
1 $params = @{'As'='Table';
2 'PreContent'='<h2>♦ NICs</h2>';
3 'EvenRowCssClass'='even';
4 'OddRowCssClass'='odd';
5 'MakeHiddenSection'=$true;
6 'TableCssClass'='grid'}
7 $html_na = Get-InfoNIC -ComputerName $Computer |
8 ConvertTo-EnhancedHTMLFragment @params
Nada nuevo en el fragmento anterior, pero ahora estamos listos para generar el HTML final:
1 $params = @{'CssStyleSheet'=$style;
2 'Title'="System Report for $computer";
3 'PreContent'="<h1>System Report for $\
4 computer</h1>";
5 'HTMLFragments'=@($html_os,$html_cs,$html_dr,\
6 $html_pr,$html_sv,$html_na);
7 'jQueryDataTableUri'='C:\html\jqueryd\
8 atatable.js';
9 'jQueryUri'='C:\html\jquery.js'}
10 ConvertTo-EnhancedHTML @params |
11 Out-File -FilePath $filepath
12
13 <#
14 $params = @{'CssStyleSheet'=$style;
15 'Title'="System Report for $computer";
16 'PreContent'="<h1>System Report for $\
17 computer</h1>";
18 'HTMLFragments'=@($html_os,$html_cs,$html_dr,\
19 $html_pr,$html_sv,$html_na)}
20 ConvertTo-EnhancedHTML @params |
21 Out-File -FilePath $filepath
22 #>
23 }
24 }
25
26 }
El código no comentado y el código comentado hacen lo mismo. El primero, no comentado, establece una ruta de archivo local para los dos archivos JavaScript necesarios. El comentado no especifica esos parámetros, por lo que el código HTML final utilizará el JavaScript desde la Red de distribución de contenido (CDN) basada en la Web de Microsoft. En ambos casos:
- -CssStyleSheet especifica mi CSS - estoy alimentando mi variable predefinida $style. También puede vincular a una hoja de estilo externa (hay un parámetro diferente, -CssUri, para eso), pero tener el estilo incrustado en el HTML lo hace más autónomo.
- -Title especifica qué se mostrará en la barra de título del navegador o pestaña.
- -PreContent, que estoy definiendo mediante las etiquetas HTML
<H1>, aparecerá en la parte superior del informe. También hay un -PostContent si desea agregar un pie de página. - -HTMLFragments requiere una matriz (de ahí el uso de @ () para crear una matriz) de fragmentos HTML producidos por ConvertTo-EnhancedHTMLFragment. Así estoy alimentando las 6 secciones del informe HTML que creé anteriormente.
El resultado final se canaliza a la ruta de archivo que creé anteriormente. Así se ve el resultado:
Tengo mis últimas dos secciones contraidas. Observe que la lista de procesos está paginada, con los botones Previous/Next y además mi disco sin el 80% está resaltado en rojo. Las tablas muestran 10 filas por defecto, pero se pueden hacer más grandes, y ofrecen un cuadro de búsqueda incorporado. Se puede hacer clic sobre los encabezados de columna para ordenar.
¡Francamente, creo que se ve extraordinario!