28 lug 2009

Variabili d'ambiente nello script di Logon

Come settare le variabili di ambiente in uno script di logon?
In VBScript si può fare così:

Set WshShell = WScript.CreateObject("WScript.Shell")
Set WshUEnv = WshShell.Environment("User")

WshUEnv("INIAPP") = "%USERPROFILE%\APP.INI"

(in questo esempio ho settato alla variabile INIAPP, relativa all'ambiente dell'utente, il valore "%USERPROFILE%\APP.INI"). Per rendere la modifica permanente, aggiungo anche la seguente riga:
WshShell.RegWrite "HKCU\Environment\INIAPP", "%USERPROFILE%\APP.INI", "REG_EXPAND_SZ"

Capita spesso inoltre di dover modificare il PATH di ricerca. La soluzione che utilizzo è la seguente, e mi permette di evitare di avere PATH doppi:

Set WshShell = WScript.CreateObject("WScript.Shell")
Set WshUEnv = WshShell.Environment("User")
Set pp = CreateObject("Scripting.Dictionary")

for each p in Split(WshUenv("PATH"), ";")
  AddPath p
next

AddPath "C:\PROGRAMMA1"
AddPath "C:\ORACLE"
AddPath "C:\TEST"

WshUEnv("PATH") = Join(pp.Keys, ";")
WshShell.RegWrite "HKCU\Environment\PATH", Join(pp.Keys, ";"), "REG_EXPAND_SZ"

pp.RemoveAll
Set pp = Nothing
Set WshShell = Nothing
Set WshUEnv = Nothing

Sub AddPath(path)
 If Not pp.Exists(path) and Trim(path)<>"" then pp.Add path, ""
End Sub

Suddivido il percorso originario in un array con i percorsi singoli, e con l'oggetto Scripting.Dictionary aggiungo ogni singolo percorso, senza duplicati. Poi ricompongo la stringa.
Semplice ma elegante!

Mappare Condivisioni di Rete in base al Gruppo di appartenenza

Molte volte nello script di logon c'è la necessità di mappare condivisioni di rete in base ai gruppi cui appartiene un utente.
L'articolo How Can I Map Drives Based on Membership in a Group? tratto dal sito di Microsoft spiega molto bene il problema e fornisce una valida introduzione; lo script presentato però è molto semplificato. Ho cercato di estenderlo in base alle esigenze della mia azienda.

Gruppo Primario
Il codice tratto dall'articolo di Microsoft restituisce tutti i gruppi, ad esclusione del gruppo primario dell'utente:

Set objSysInfo = CreateObject("ADSystemInfo")
Set objNetwork = CreateObject("Wscript.Network")

strUserPath = "LDAP://" & objSysInfo.UserName
Set objUser = GetObject(strUserPath)

For Each strGroup in objUser.MemberOf
  strGroupPath = "LDAP://" & strGroup
  Set objGroup = GetObject(strGroupPath)
  strGroupName = objGroup.CN
  Wscript.Echo strGroupName 
Next
Se la objectUser.MemberOf contiene soltanto un gruppo questo il codice qui sopra va in errore, poiché MemberOf non è un array. Bisogna quindi distinguere se si tratta di un array oppure di un gruppo singolo in questo modo:
If isArray(objUser.MemberOf) then
  For Each strGroup in objUser.MemberOf
      strGroupPath = "LDAP://" & strGroup
      Set objGroup = GetObject(strGroupPath)
      checkGroup Unit, objGroup.CN
  Next
Else
    strGroupPath = "LDAP://" & objUser.MemberOf
    Set objGroup = GetObject(strGroupPath)
    checkGroup Unit, objGroup.CN
End If

Alla collezione MemberOf manca dunque il gruppo primario dell'utente, che generalmente è Domain Users ma potrebbe sempre essere stato modificato. Possiamo però ottenere il codice del gruppo primario con objUser.PrimaryGroupID e scorrere l'elenco dei gruppi fino a che non troviamo il gruppo con questo ID. La funzione che restituisce il nome del gruppo dato l'ID è la seguente:

Function GetPrimaryGroup(ByVal GroupID)
Set objConnection = CreateObject("ADODB.Connection")
Set objCommand = CreateObject("ADODB.Command")

objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"

Set objCommand.ActiveConnection = objConnection
objCommand.Properties("Page Size") = 100
objCommand.Properties("Timeout") = 30
objCommand.Properties("Cache Results") = False

Set objRootDSE = GetObject("LDAP://RootDSE")
strDNSDomain = objRootDSE.Get("defaultNamingContext")

objCommand.CommandText = "<ldap://" & strdnsdomain & ">;" &_
  "(objectClass=group);" &_
  "sAMAccountName,primaryGroupToken;subtree"

Set objRecordSet = objCommand.Execute

Do Until objRecordSet.Eof
  If objRecordSet("primaryGroupToken").Value = GroupID Then
    GetPrimaryGroup = objRecordSet("sAMAccountName").Value
    Exit Do
  End If
  objRecordSet.MoveNext
Loop

objRecordset.Close
objConnection.Close

Set objRecordset = Nothing
Set objConnection = Nothing
End Function

Non sono soddisfatto di questa funzione, in quanto non sono riuscito a filtrare il gruppo direttamente nel CommandText e sono pertanto costretto a scorrere tutti i gruppi tramite Loop, fino a che non trovo il gruppo con il codice ID che stavo ricercando, ma.... comunque funziona.

Associazione Gruppi-Condivisioni
Tramite l'oggetto Scripting.Dictionary creo un elenco di associazioni tra gruppi e relative aree condivise, come ad esempio:

Set d = CreateObject("Scripting.Dictionary")

d.Add "Finanziario", "\\server01\fs\Finanziario"
d.Add "Personale", "\\server01\fs\Personale"
d.Add "Legale", "\\server02\Legale"

MapNetworkDrive
A questo punto posso scorrere tutto l'elenco dei gruppi cui l'utente appartiene, passando ogni singolo gruppo alla seguente procedura. Se il gruppo appartiene alla lista dei gruppi a cui è associata un'area condivisa, tale area viene montata nel disco "Unita".

Sub checkGroup(ByRef Unita, ByVal Group)
On Error Resume Next
  If d.Exists(Group) Then
    objNetwork.RemoveNetworkDrive Unita & ":"
    objNetwork.MapNetworkDrive Unita & ":", d(Group), False
    Unita = Chr(Asc(Unita) + 1)
  End If
End Sub

Codice Finale
Il codice finale è il seguente, a cui vanno aggiunte la sub e la funzione presentate in precedenza:

On Error Resume Next

Set objSysInfo = CreateObject("ADSystemInfo")
Set objNetwork = CreateObject("Wscript.Network")

rem *** associazioni gruppo-percorso
Set d = CreateObject("Scripting.Dictionary")

d.Add "Finanziario", "\\server01\fs\Finanziario"
d.Add "Personale", "\\server01\fs\Personale"
d.Add "Legale", "\\server02\Legale"

rem *** prima unità di rete da connettere
Unit = "O"

rem *** utente corrente
Set objUser = GetObject("LDAP://" & objSysInfo.UserName)

rem *** controlla il gruppo primario ***
checkGroup Unit, GetPrimaryGroup(objUser.PrimaryGroupID)

rem *** controlla gli altri gruppi ***
For Each strGroup in objUser.MemberOf
strGroupPath = "LDAP://" & strGroup
Set objGroup = GetObject(strGroupPath)
checkGroup Unit, objGroup.CN
Next

rem *** cleanup
d.RemoveAll

Se un utente appartiene a più gruppi che dispongono di un'area associata, questo script monta tutti i percorsi di rete a partire dalla lettera O:, poi P:, Q: etc...
L'area di lavoro associata al gruppo primario verrà sempre montata come unità O:, mentre le altre aree saranno montate a seguire.
Se la stessa area di lavoro è associata a più gruppi, ed un utente appartiene a più di uno di questi gruppi, la stessa area verrà montata più volte.

Happy logging on!

2 lug 2009

Inviare Check Passivi a Nagios

Nagios è un programma di monitoraggio di computer o risorse di rete che permette di inviare degli avvisi quando un nodo od un servizio non risulta attivo, oppure al ripristino del suo funzionamento.

Per il monitoraggio degli apparati è possibile utilizzare uno dei numerosi comandi predefiniti (es. ping, snmp, etc) oppure è possibile scrivere un comando personalizzato - uno script bash, perl, python o altro va benissimo, l'importante è che tale script restituisca un valore che indica se il servizio che stiamo monitorando è attivo oppure se presenta qualche problema.

Il sistema si occupa in automatico di schedulare i test, di raccogliere i risultati, e di inviare eventuali avvisi.

Oltre a questa modalità di monitoraggio in cui il Nagios effettua dei test ed attende la risposta dagli host remoti, che è detta attiva, è possibile utilizzare una seconda modalità, detta passiva, in cui sono gli host o i servizi remoti che inviano il loro stato al Nagios.

L'utilizzo più tipico consiste nel monitoraggio delle operazioni pianificate, ad esempio backup, allineamento tabelle, etc.
Un task viene eseguito, svolge il backup, l'allineamento, o quant'altro, ed al termine invia al Nagios una notifica con l'esito dell'operazione. Se il Nagios non riceve tale esito entro un certo periodo di tempo (perché il task si blocca prima di inviare l'avviso, o perché il task dura troppo a lungo) può segnalare comunque di non aver ricevuto alcuna informazione e attivare un allarme.

Il servizio può essere definito in questo modo (i parametri possono essere personalizzati a piacere):
define service{
  use                    service-template
  host_name              myhost
  service_description    logs
  freshness_threshold    93600
  notification_period    24x7
  check_command          check_dummy!3 "Dati non ricevuti"
  active_checks_enabled  0
  passive_checks_enabled 1
  notification_interval  0
  check_freshness        1
  check_period           24x7
  max_check_attempts     1
  contact_groups         contatti
  notification_options   c,u,w,r
  notification_interval  0
  notification_period    24x7
}

In cui il comando check_dummy è definito nel seguente modo:
define command {
  command_name    check_dummy
  command_line    $USER1$/check_dummy $ARG1$
}
in questo esempio, Nagios attende il ricevimento di una notifica passiva, e se non riceve una notifica da più di 93600 secondi, allora esegue il comando check_dummy, che imposta lo stato del servizio ad UNKNOWN ed inserisce l'avviso che non riceve i log. Un amministratore di sistema si dovrà dunque preoccupare di approfondire il problema.

Come inviare i dati al server Nagios centrale? Esistono molti sistemi, ma il più semplice ed il mio preferito consiste nell'inviare l'esito dell'operazione direttamente al cmd.cgi che lo mette in coda agli altri eventi e lo fa processare.

Basta inviare un semplice POST HTTP al cgi che si trova in cgi-bin/cmd.cgi e siamo a posto! Come al solito il codice seguente è decisamente semplificato, e non tiene conto di ogni possibile errore o circostanza. In alternativa, è sempre possibile lanciare un buon wget --post="parametri" http://nagios/nagios/cgi-bin/cmd.cgi.
import httplib
import urllib
import sys

argc = len(sys.argv)

if (argc < 6):
  print "Nagios Passive Check Submit"
  print "Esempio:"
  print "nagiospcs Server \"Trasferimento Dati\" 3 \"Errore Import\" \"\""
else:
  data = urllib.urlencode(
    {"cmd_typ" : "30",
     "cmd_mod" : "2",
     "host" : sys.argv[1],
     "service" : sys.argv[2],
     "plugin_state" : sys.argv[3],
     "plugin_output" : sys.argv[4],
     "performance_data" : sys.argv[5]})
  f = urllib.urlopen(
    "http://user:password@servernagios/nagios/cgi-bin/cmd.cgi",
    data)
  s = f.read()
  f.close()import httplib
import urllib
import sys

argc = len(sys.argv)

if (argc < 6):
  print "Nagios Passive Check Submit"
  print "Esempio:"
  print "nagiospcs Server \"Trasferimento Dati\" 3 \"Errore Import\" \"\""
else:
  data = urllib.urlencode(
    {"cmd_typ" : "30",
     "cmd_mod" : "2",
     "host" : sys.argv[1],
     "service" : sys.argv[2],
     "plugin_state" : sys.argv[3],
     "plugin_output" : sys.argv[4],
     "performance_data" : sys.argv[5]})
  f = urllib.urlopen(
    "http://user:password@servernagios/nagios/cgi-bin/cmd.cgi",
    data)
  s = f.read()
  f.close()
Ave.