viernes, 5 de noviembre de 2010

PowerShell y el Framework .NET

PowerShell se apoya en Framework.NET. ¿Qué significa esto? Sencillamente que PowerShell se convierte en un lenguaje más de programación como C# o Visual Basic. Aunque PowerShell está pensado como lenguaje de scripting, podemos hacer lo mismo que con cualquier otro lenguaje de programación .NET.

Ejemplo. PowerShell no incluye cmdlets para realizar cálculos matemáticos complejos, pero Framework.NET sí. En un programa hecho en C# podemos acceder a las funciones matemáticas incluyendo el espacio de nombres System.Math ubicado en la DLL mscorlib.dll.

En PowerShell esto lo hacemos con el objeto [System.Math]

Necesito información de métodos y propiedades de [System.Math] para saber qué puedo usar: [System.Math] | gm

Vemos que hay un método llamado GetMembers(). Con esto ya podemos consultar a la clase:

[System.Math].GetMembers()

Y hacer uso de todos los métodos y propiedades estáticas(no hace falta instanciar un objeto):

[System.Math]::PI
[System.Math]::sqrt(25)


Y lo mismo con cualquier otra clase de FrameWork.NET. ¿Os suena esto?:

[System.Console]::WriteLine("Hola Mundo")

¿Y si quiero hacer uso de métodos o propiedades no estáticos? Pues a crear un objeto.

$Ping = New-Object System.Net.NetworkInformation.Ping
$Ping.Send("10.10.10.10")


¿Y si la DLL con las clases que quiero usar no está cargada?

Para ver ensamblados cargados: [System.AppDomain]::CurrentDomain.GetAssemblies()
o mejor

[System.AppDomain]::CurrentDomain.GetAssemblies() | foreach-object { split-path $_.Location -leaf } | sort

Para cargar un ensamblado:

Add-Type -AssemblyName Nombre_del_ensamblado (si está en la GAC)
Add-Type -Path "ruta_completa_al_ensamblado"

Funciones

En PowerShell podemos hacer uso de funciones para evitar la repetición de un conjunto de instrucciones continuamente. Un ejemplo de función es:

Function salida
{
Write-Host "Mensaje"
}


A una función le podemos pasar valores y hay tres maneras de hacerlo.

Primera manera: Podemos pasar a una función todos los parámetros que queramos y hacemos referencia a ellos mediante $args[n]

Function suma
{
[int]$args[0] + [int]$args[1]
}


A esta función la podemos llamar así:

suma 5 10

Segunda manera: Podemos pasar a una función una serie de parámetros especificados

Function suma ([int]$x, [int]$y)
{
$x+$y
}


A esta función le llamamos de una de las siguientes maneras:

suma 5 10

suma -x 5 -y 10


Tercera manera:

Function suma
{
Param ([int]$x, [int]$y)
$x+$y
}


A esta función le llamamos de una de las siguientes maneras:

suma 5 10

suma -x 5 -y 10


Las funciones en PowerShell devuelven cualquier valor que se envíe al stream de salida. En los ejemplos anteriores solo se devuelve un valor, pero no tenemos límite en el número de valores devueltos, todos se van agregando a un array.

Nota. Podemos usar la palabra clave Return para salir inmediatamente de una función devolviendo un único valor.

Versión del sistema operativo

Si en un script de PowerShell necesitamos conocer la versión del sistema operativo para realizar tareas diferentes dependiendo de dicha versión, lo podemos hacer de la siguiente manera:

$maquina = $env:computername
$filtro = "(&(objectCategory=Computer)(Name=$maquina))"
$objSearcher = New-Object System.DirectoryServices.DirectorySearcher
$objSearcher.Filter = $filtro
$objPath = $objSearcher.FindOne()
$objEquipo = $objPath.GetDirectoryEntry()
$objEquipo.operatingSystem

Mapeo de impresoras

El siguiente ejemplo, muestra como mapear impresoras de red con un script de PowerShell basándose en el departamento al que pertenecen los usuarios.

Condiciones:

1) Los usuarios del Directorio Activo tienen que tener el atributo department rellenado con el departamento al que pertenecen.
2) Las impresoras tienen que estar publicadas en el Directorio Activo y el atributo location tiene que contener un nombre de departamento.

El script recorre todas las impresoras y mapea al usuario las impresoras cuyo atributo location coincide con el atributo department del usuario.

###Función que mapea la impresora

function mapear_impresora([string]$ruta)
{

$net = New-Object -com WScript.Network;
$impresoras=$net.EnumPrinterConnections()
if ($impresoras -like $ruta) {$net.RemovePrinterConnection($ruta,$true)}
$net.AddWindowsPrinterConnection($ruta)
}


#Obtenemos el usuario que inicia la sesión.

$usuario = $env:username
$filtro = "(&(objectCategory=User)(samAccountName=$usuario))"
$objSearcher = New-Object System.DirectoryServices.DirectorySearcher
$objSearcher.Filter = $filtro
$objPath = $objSearcher.FindOne()
$objUser = $objPath.GetDirectoryEntry()

###Obtenemos las impresoras del Directorio Activo

$filtroimpresora = "(&(objectCategory=PrintQueue))"
$objSearcher = New-Object System.DirectoryServices.DirectorySearcher
$objSearcher.Filter = $filtroimpresora
$objPath2=$objSearcher.FindAll()


###Mapeos por usuario

for($n=0;$n -lt $objPath2.Count;$n++) { if ( $objPath2[$n].Properties.location -eq $objUser.Properties.department ) { mapear_impresora -ruta $objPath2[$n].Properties.uncname } }

Este script se puede asociar a una directiva de grupo como script de inicio de los usuarios.

Mapeos unidades de red

PowerShell incluye varios cmdlet para la gestión de unidades ya sean locales o de red. son los cmdlets con nombre PSDrive y verbos Get, New y Remove. El problema de estos cmdlets es que solo actúan en las sesiones de PowerShell, es decir, si creamos un mapeo de red desde PowerShell con el cmdlet New-Psdrive, la unidad mapeada solo es visible en la consola de PowerShell, y no en el explorador de Windows, por ejemplo. Por lo que tenemos que hacer uso de objetos COM para el mapeo correcto de las unidades de red.

El siguiente ejemplo mapea unidades de red a un usuario basándose en la pertenencia a determinados grupos:

#Función que mapea la unidad de red.

function mapear_unidad([string]$unidad, [string]$ruta)
{
$net = New-Object -com WScript.Network;
$unidades=$net.EnumNetworkDrives();
if ($unidades -like $unidad) { $net.removenetworkdrive($unidad,$true)}
$net.mapnetworkdrive($unidad,$ruta)
}


#Obtenemos el usuario que inicia la sesión.

$usuario = $env:username
$filtro = "(&(objectCategory=User)(samAccountName=$usuario))"
$objSearcher = New-Object System.DirectoryServices.DirectorySearcher
$objSearcher.Filter = $filtro
$objPath = $objSearcher.FindOne()
$objUser = $objPath.GetDirectoryEntry()

#Obtenemos los grupos a los que pertenece.
$groups=$objUser.memberOf

#Los grupos están en formato LDAP, vamos a quedarnos con el nombre CN

function new-array {,$args}
$groupCN= new-array
foreach($i in $groups)
{ $groupCN+=($i.split(",")[0]).split("=")[1] }

#Mapeo de unidad personal
$carpetausuario=$objUser.sAMAccountName
mapear_unidad -unidad "K:" -ruta "\\Servidor\Personales\$carpetausuario"


#Mapeo de unidades de grupo

foreach ($group in $groupCN)
{

switch ($group)
{

Grupo1 { mapear_unidad -unidad "T:" -ruta "\\Servidor\CarpetaGrupo1" }
Grupo2 { mapear_unidad -unidad "M:" -ruta "\\Servidor\CarpetaGrupo2" }
default {"No hay mapeos para el grupo $group"}
}
}

jueves, 4 de noviembre de 2010

Logon Scripts con PowerShell

Es habitual tener scripts de inicio de sesión programados en Batch o en Visual Basic Script. Ahora es posible hacerlo también con PowerShell.

Cosas a tener en cuenta:

1) Los scripts de inicio van a estar en una carpeta compartida de un controlador de dominio y las máquinas del dominio acceden al script a través de la red. Es muy importante que las máquinas cliente reconozcan el acceso al DC como si fuera un acceso de Intranet, no un acceso de Internet, ya que entonces saltará el famoso aviso de "Advertencia de seguridad de Abrir archivo" y el script no llegaría a ejecutarse. (Ajustar los sitios de intranet en las propiedades de Internet Explorer, agregando rutas como file://Nombre_DC)

2) Los scripts de PowerShell son archivos con extensión ps1. EStos archivos tienen como acción predeterminada "Abrir" en lugar de "Ejecutar", por lo que el usuario, al iniciar la sesión, se encontrará con el notepad abierto con el código del script en lugar de ejecutarse. La solución es acceder al registro en todas las máquinas cliente:

HKEY_CLASSES_ROOT\Microsoft.PowerShellScript.1\Shell el valor predeterminado es Open, debemos cambiarlo a 0, que es la acción asociada a ejecutar el script usando el intérprete PowerShell.

3) Lógicamente, PowerShell tiene que estar instalado. (En Windows XP se puede instalar a través de las actualizaciones automáticas o mediante una directiva de grupo en la que asociamos como Script de inicio de máquina el ejecutable: WindowsXP-KB968930-x86-ESN.exe con parámetros /quiet /passive )

Una vez establecidas estas 3 condiciones, podemos crear directivas de grupo que tengan asociados scripts de inicio de sesión realizados con PowerShell.

Propiedad useraccountcontrol

Esta propiedad de los usuario del Directorio Activo contiene información a nivel de bit. A continuación muestro algunos bits de los más útiles (de bit menos significativo a más significativo):


ACCOUNTDISABLE Segundo bit puesto a 1 indica cuenta deshabilitada.
LOCKOUT Cuarto bit puesto a 1 indica cuenta bloqueada.
PASSWD_CANT_CHANGE Sexto bit puesto a 1 indica que no puede cambiar la contraseña.
NORMAL_ACCOUNT Décimo bit puesto a 1 indica cuenta normal.
DONT_EXPIRE_PASSWORD Décimoseptimo bit puesto a 1 indica que la contraseña no caduca.
PASSWORD_EXPIRED Vigésimo tercero bit puesto a uno indica que la contraseña caduca.


Para saber si un determinado bit está puesto a uno o a cero, por ejemplo para el bit número 17:

$numero -band 0x10000

Si el resultado es distinto de cero, el bit número 17 está puesto a 1. Si el resultado es cero, entonces el bit está puesto a cero.


Para el ejemplo de hace dos posts, podemos hacer:


$resultado=$($informacion.Properties.useraccountcontrol) -band 0x10000
if ($resultado -ne 0) { Write-Host " La contraseña no tiene fecha de caducidad"}

Propiedades especiales de fechas

En el Directorio Activo hay una serie de propiedades(atributos) que contienen una fecha en formato de número entero muy grande. Es el caso de:


pwdlastset (Fecha del último cambio de contraseña)
accountexpires (fecha de caducidad de la cuenta)
lastlogon (Fecha del último inicio de sesión)

Estos números están indicando fechas. Concretamente indican (ojo al dato) el número de intervalos de 100 nanosegundos transcurridos desde el 1 de Enero de 1601 (coincidiendo con el comienzo del calendario gregoriano).

Vamos a pasar uno de esos números a una fecha usando la siguiente función:


Function epoch2date($segundos) { [timezone]::CurrentTimeZone.ToLocalTime(([datetime]'1/1/1601').AddSeconds($segundos)) }

Y la usamos de la siguiente manera:

epoch2date(valor_en_segundos)

Para el ejemplo que veíamos en el post anterior, podemos hacer:

epoch2date($($informacion.Properties.lastlogon)/1E+7)

Y el resultado será algo como:

miércoles, 03 de noviembre de 2010 10:09:51

miércoles, 3 de noviembre de 2010

Información del usuario

Muchas veces necesitamos hacer un script que obtenga información del usuario que ha iniciado la sesión. En PowerShell, lo podemos hacer de varias maneras y quizás una de las más fáciles es la siguiente:

$usuario = $env:username
$consulta = "(&(objectCategory=User)(samAccountName=$usuario))"
$objSearcher = New-Object System.DirectoryServices.DirectorySearcher
$objSearcher.Filter = $consulta
$informacion = $objSearcher.FindOne()


El nombre del usuario que ha iniciado la sesión se almacena en la variable de entorno username. La leemos de la unidad de las variables de entorno (env:) y buscamos el objeto correspondiente en el Directorio Activo. Una vez realizada la consulta, tenemos toda la información en el objeto $informacion. Podemos hacer cosas como:

Ver todas las propiedades:

$informacion.Properties

Acceder a una de ellas (por ejemplo, el nombre completo):

$informacion.Properties.distinguishedname

Suele ser habitual, consultar los grupos a los que pertenece el usuario:

$informacion.Properties.memberof