Get windows time settings from remote servers

Standard

I recently had to deal with some issues with time on our servers and I wanted to get an overview of our environment. To do that I wrote the script in this post which gets the time, timesource (NTP servers) and time zone information from remote windows servers and exports the information to a handy CSV file.

How does the script work?

1. The script loops through all the computers listed in “servers.txt” which much be created and placed in the script directory.
2. Using the function “Test-PortAlive” (I already covered this in a different blog post) the script checks that the ports 135 (rpc) and 445 (smb) are open on the server. This is to prevent the script from timing out and hanging on servers where these ports are blocked. This allows for a quick script run even with many servers.
3. If the ports are open and queries with .net and wmi are possible the script then gets the time, time source and time zone information from the server.
4. When all the information has been retrieved the results are exported to a CSV file in the script directory.

How did I build the script?

I divided the script into three sections Functions, Variables and Script Main to simplify the structure of the script.
The script has four functions which provide the main functionality.
I. Get-TimeSource
This function uses .net to query the remote registry for the NTP server settings.
II. Get-TimeZone
WMI is used to get the time zone configured on the remote server.
III. Get-Time
WMI is used to get the time on the remote server returned as a datetime object.
IV. Test-PortAlive
A function I wrote a while back which checks if given ports are open on a remote server. Here it is used to check if ports 135 and 445 are open on the remote server as this is needed to perform the queries for time configuration information.

In the #script main section I then loop through the servers and for each one execute the above functions to get the info. At then end the information is exported to CSV.

The script itself!

#############################################################################################
##Get-RemoteTime.ps1
##
##Description:		Loops through all the computers listed in "servers.txt" in the script 
#+					directory and for each one gets the timesource, timezone and the time 
#+					using .NET and WMI. Exports the results to a CSV file at the end of the
#+					script run.
##Created by:		Noam Wajnman
##Creation Date:	July 16, 2013
##Updated:			July 31, 2014
##############################################################################################
#FUNCTIONS
function Get-TimeSource {
	#############################################################################################
	##Function:			Get-RemoteTimeSource
	##
	##Description:		Pipeline function. Gets the configured NTP server timesource from the 
	#+					registry of a remote server.
	##
	##Created by:		Noam Wajnman
	##Creation Date:	31 July, 2014	
	##############################################################################################
	[CmdletBinding()]
	[OutputType([System.String])]
	param (
		[Parameter(ValueFromPipeline=$true)]$server
	)
	begin {        
	    $key = "SYSTEM\\CurrentControlSet\\Services\\W32Time\\Parameters" #path/name of the reg key
    }
	process {
		$timesource = '' | select "computer","NTPServer","Type" #Create a custom object properties "computer", "NTPServer" and "Type"		
		$timesource.computer = $server #set the computer property on the custom object		
		$reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $server) #connect to the remote registry
		$regKey= $reg.OpenSubKey($key) #get the registry key		
		$str_value1 = $regKey.GetValue("NTPServer")	#get the value of "NTPServer"	
		$value1 = $str_value1 -split ","		
		$timesource.NTPServer = $value1[0] #split the value and set the NTPServer property on the custom object to the first returned timesource.		
		$value2 = $regKey.GetValue("Type") #get the value of "Type"		
		$timesource.Type = $value2 #set the Type property on the custom object		
		$reg.close() #close the remote registry connection when done.
		return $timesource #return the custom object.
	}
}
function Get-TimeZone {
	#############################################################################################
	##Function:			Get-RemoteTimeZone
	##
	##Description:		Pipeline function. Gets the configured time zone from the 
	#+					remote server using WMI.
	##
	##Created by:		Noam Wajnman
	##Creation Date:	31 July, 2014	
	##############################################################################################
	[CmdletBinding()]
	[OutputType([System.String])]
	param (
		$server
	)
	process {
		$timeZone = (gwmi win32_timeZone -ComputerName $server).caption	
		return $timeZone
	}
	
}
function Get-Time {
	#############################################################################################
	##Function:			Get-RemoteTime
	##
	##Description:		Pipeline function. Gets the time from the remote server using WMI.
	##
	##Created by:		Noam Wajnman
	##Creation Date:	31 July, 2014	
	##############################################################################################
	[CmdletBinding()]
	[OutputType([datetime])]
	param (
		$server
	)
	process {
		$remoteOSInfo = gwmi win32_OperatingSystem -computername $server	
		[datetime]$remoteDateTime = $remoteOSInfo.convertToDatetime($remoteOSInfo.LocalDateTime)	
		return $remoteDateTime
	}
}
function Test-PortAlive {
	#############################################################################################
	##Function:			Test-PortAlive
	##
	##Description:		Tests connection on a given server on a given port.
	##
	##Created by:		Noam Wajnman
	##Creation Date:	April 02, 2014	
	##############################################################################################
	[CmdletBinding()]
	[OutputType([System.boolean])]
	param(
		[Parameter(ValueFromPipeline=$true)][System.String[]]$server,
		[int]$port
	)
	$socket = new-object Net.Sockets.TcpClient
	$connect = $socket.BeginConnect($server, $port, $null, $null)
	$NoTimeOut = $connect.AsyncWaitHandle.WaitOne(500, $false)
	if ($NoTimeOut) {
		$socket.EndConnect($connect) | Out-Null
		return $true				
	}
	else {
		return $false
	}
}
#VARIABLES
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath
$servers = gc "$dir\servers.txt"
$CSV = "$dir\ServersTimeInfo.csv"
#SCRIPT MAIN
clear
$TimeInfo = @()
$servers | % {
	$alive_rpc = Test-PortAlive -server $_ -port 135
	$alive_smb = Test-PortAlive -server $_ -port 445
	if ($alive_rpc -and $alive_smb) {
		Write-Host "Getting time info from $_"	
		$RemoteTimeInfo = '' | select "computer","NTPServer","NTP_Type","TimeZone","Time"
		$RemoteTimeInfo.computer = $_
		$timesourceInfo = Get-TimeSource -server $_	
		$RemoteTimeInfo.NTPServer = $timesourceInfo.NTPServer
		$RemoteTimeInfo.NTP_Type = $timesourceInfo.Type
		$RemoteTimeInfo.TimeZone = Get-TimeZone -server $_
		$RemoteTimeInfo.Time = Get-Time -server $_		
		$TimeInfo += $RemoteTimeInfo
	}
	else {
		Write-Host "Error - Couldn't get WMI info from $_"
	}
}
$TimeInfo | Export-Csv $CSV -NoTypeInformation

How do I run the script?

Save the script to your computer as Get-RemoteTime.ps1.
Create servers.txt and place it in the same directory as the script. Populate servers.txt with the names of the servers you wish to get time information from and save. You can now open a powershell prompt, browse to the script directory and execute it. You can also simply open the script in an editor such as powershell ISE or PowerGUI and run it by pressing F5.
That’s it! I hope you find the script useful 🙂

Advertisement

Powershell – Import certificates on remote servers

Standard

A while back I was tasked with importing some certificates on all our web servers. I didn’t like the idea of doing this manually on hundreds of servers so I decided I had to write a script which could help me with this job. The problem was that I couldn’t find any “easy” way of importing certificates on remote servers. It is possible with some commercial tools and also with windows 2012 R2 and powershell v4 but none of these options were available to me. Furthermore I wanted to have a script which was generic and could be used on all windows servers regardless of version. This led me to create this powershell script which I will go over for you now.

The script loops through the servers in the “servers.txt” file (you must create this file and put it in the script directory) and basically works as outlined in these steps:
1. The certificate is copied to the remote server
2. A custom mini-scipt for importing the certificate is created and copied to the remote server.
3. The mini-script is invoked by using psexec and imported into the desired certificate store.
4. The certificate and mini-script are deleted on the remote server after use.
5. The result of the certificate import is checked.

A log file with relevant information is created when the script is run, making it possible to confirm the successful import later on.
The script is divided into three sections – variables, functions and script main.

Variables

#VARIABLES
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath
$RemoteCertDir = "c:\temp\certs\" #temporary file location on remote server
$logfile = "$dir\logs\" + "cert-bulkimport_" + $(get-date -Format "yyyy-MM-dd_HH-mm-ss") + ".log"
$servers = gc "$dir\servers.txt"
$psexec = "$dir\psexec.exe"
#parameters
$DebugPreference = "continue" #comment out to disable debug info
$certRootStore = "localmachine"
$certStore = "My"
$Cert = "$dir\certs\some_cert.pfx" #certificate to import
$CertPW = "Password" #password for certificate to import
$CertCN = "some.domain.com" #common name of certificate to import

The first two lines of code –
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath

are really practical as it gives me an object $dir which always points to where the script was run. This makes the script more robust as I can freely move the script around and run it from many locations without having to change any paths in the script.
$logfile is the path of the log file which will be created for each script run. The file name will contain a time stamp to avoid overwriting the file.
$servers is an array populated by running Get-Content on the “servers.txt” file.
$psexec is the path to psexec.exe (place it in the same directory as the script).
The parameters section is really important and you need to fill these variables with the specific information about your certificate and which store to import them to. The descriptions in the code comments should explain how to do this.

Functions

1. Function Log-Entry

function log-entry {
    param(
        $message
    )
    $time = Get-date
    Add-Content -Path $logfile -Value "$time`t$message"
}

This function takes a parameter “message” and appends it to the log file with a timestamp. I use it to easily write to the log file throughout the script.

2. Function RemoteCopy-Certificate

function RemoteCopy-Certificate {
    param(
        $server
    )
    $copyresult = $false
    $RemoteDir = "\\$server\" + $RemoteCertDir.replace(":","$")	
    Write-Debug "Creating directory $RemoteDir"
    log-entry -message "Creating directory $RemoteDir"	
    New-Item -ItemType "directory" $RemoteDir -Force
    $RemoteCertPath = $RemoteDir + (gci $Cert).Name	
    Write-Debug "Copying $Cert to $RemoteDir"
    log-entry -message "Copying $Cert to $RemoteDir"	
    Copy-Item -Path $Cert -Destination $RemoteCertPath -Force
    if (Test-Path -Path $RemoteCertPath) {
        $CopyResult = $true		
        Write-Debug "Copy of $Cert to $RemoteDir succeeded."
        log-entry -message "Copy of $Cert to $RemoteDir succeeded." 
    }
    else {
        $CopyResult = $false 		
        Write-Debug "Error - Copy of $Cert to $RemoteDir failed."
        log-entry -message "Error - Copy of $Cert to $RemoteDir failed."
    }
    return $CopyResult
}

This function creates a temp directory on the remote server and copies the certificate to there. It then checks if the certificate is there and returns $true if the copy was successful.

3. Function Create-ImportScript

function Create-ImportScript {
    param(
        $server
    )
    $ScriptCreateResult = $false
    $RemoteScriptPath = "\\$server\" + $RemoteCertDir.Replace(":","$") + "import-pfx.ps1"    
    $CertPath = "'" + $RemoteCertDir + (gci $Cert).Name + "'"    
    $crtPass = "'" + $CertPW + "'"
    $SB = @"
`n
`$crt = new-object System.Security.Cryptography.X509Certificates.X509Certificate2
`$crt.import($certPath,$crtPass,"Exportable,PersistKeySet")
`$store = new-object System.Security.Cryptography.X509Certificates.X509Store(`'$certStore`',`'$certRootStore`') 
`$store.open("MaxAllowed") 
`$store.add(`$crt) 
`$store.close()
"@
    log-entry -message "Creating import script $RemoteScriptPath"
    Write-Debug "Creating import script $RemoteScriptPath`:$SB`n"	
    $SB | Out-File $RemoteScriptPath -force
    if (Test-Path -Path $RemoteScriptPath) {
        $ScriptCreateResult = $true		
        Write-Debug "Creation of $RemoteScriptPath succeeded."
        log-entry -message "Creation of $RemoteScriptPath succeeded." 
    }
    else {
        $ScriptCreateResult = $false		
        Write-Debug "Error - Creation of $RemoteScriptPath failed."
        log-entry -message "Error - Creation of $RemoteScriptPath failed."
    }
    return $ScriptCreateResult    
}

Here, a custom powershell mini import script is created. Basically the mini script comes from the multi-line string $SB which is redirected to create a .ps1 file. The function then checks if the file was created on the remote server and returns $true for a successful result.

4. Function Invoke-Importscript

function Invoke-Importscript {
    param(
        $server
    )
    $ImportScriptPath = $RemoteCertDir + "import-pfx.ps1"
    $arg1 = "/acceptEula"
    $arg2 = "\\$server"
    $arg3 = "cmd /c"
    $arg4 = "`"echo .|powershell -file $ImportScriptPath`""	
    Write-Debug "invoking remote import script on $server"   
    log-entry -message "invoking remote import script on $server"	
    Start-Process -Wait -FilePath $psexec -ArgumentList "$arg1 $arg2 $arg3 $arg4"
}

This function runs the mini script on the remote server and imports the certificate into the chosen store. Psexec.exe is used to invoke the script on the remote server.

5. Function Remove-TempFiles

function Remove-TempFiles {
    param(
        $server
    )
    $RemoteScriptDir = "\\$server\" + $RemoteCertDir.Replace(":","$")	
    Write-Debug "Removing temporary cert and script files from $RemoteScriptDir"
    log-entry -message "Removing temporary cert and script files from $RemoteScriptDir"	
    gci -Recurse -Path $RemoteScriptDir | Remove-Item
}

As we don’t want to leave any certs or scripts behind after we are done I wrote this function to remove these files after the cert has been imported.

6. Function List-RemoteCerts

function List-RemoteCerts {
    param(
        $server
    )
    $ro = [System.Security.Cryptography.X509Certificates.OpenFlags]"ReadOnly"
    $lm = [System.Security.Cryptography.X509Certificates.StoreLocation]"LocalMachine"
    $store = new-object System.Security.Cryptography.X509Certificates.X509Store("\\$server\$CertStore", $lm)
    $store.Open($ro)
    return $store.Certificates
}

This function lists the certificates on the remote server. I use this function in the next function Confirm-CertImport.

7. Function Confirm-CertImport

function Confirm-CertImport {
    param(
        $server
    )
    Write-Debug "Checking if certificate import on $server was successful."
    log-entry -message "Checking if certificate import on $server was successful."	
    $IsCertThere = $false    
    $MyCerts = List-RemoteCerts -server $server        
    $MyCerts | % {	
        Write-Debug "Found certificate: $($_.Subject)"		
        if ($_.Subject -like "*$CertCN*") {		
            Write-Debug "*************Certificate match for $CertCN found!)"			
            $IsCertThere = $true
        }
    }
    return $IsCertThere       
}

Here I loop through the certificates found on the remote server. If the certificate we just attempted to import is found a result of $true is returned.

Script Main

#SCRIPT MAIN
new-item -itemtype "file" $logfile -force | out-null #create the log file for this script execution
Write-Debug "Script run started."
log-entry -message "Script run started."
$servers | % { #loop through the servers which we will import the certificate to
    Write-Debug "Starting cert import on $_".ToUpper()
    log-entry "Starting cert import on $_".ToUpper()
    $copyResult = RemoteCopy-Certificate -server $_ # Find out if the copy of the cert to the server was successful
    if ($copyResult) {   #If the copy of the certificate to the server succeeded then proceed.     
        $ScriptCreateResult = Create-ImportScript -server $_ #Find out if the script was created and copied to the server successfully
        if ($ScriptCreateResult) { #if the script was created/copied to the server successfully then proceed.
            Invoke-Importscript -server $_ #import the certificate into the specified store
            Remove-TempFiles -server $_ #Remove temp files such as the cert and import script
            $CertImportResult = Confirm-CertImport -server $_ #check that the cert was imported successfully and write the result to the log.
            if ($CertImportResult) {
                Write-Debug "Success! $CertCN was successfully imported on $_"
                log-entry -message "Success! $CertCN was successfully imported on $_"
            }
            else {
                Write-Debug "Error! Import of $CertCN on $_ failed."
                log-entry -message "Error! Import of $CertCN on $_ failed."
            }
        }
    }
    Write-Debug "-------------------------------------------------------------------------------------------------------------------------"
    log-entry "`n`n"
}

The script main starts with creating the log file used for the script run. I then loop through the $servers array and with some nested IF sentences run the functions described above.
When the script run has completed, the certificate will be imported on the servers specified in the “servers.txt” file and you will have a log file with the import results available.
I hope you find the script useful. Enjoy!!
I have copied in the full script below.

##################################################################################################################
##RemoteImport-Certificate.ps1
##Description: 		Script imports a certificate into all remote servers specified in the 
#+                  "servers.txt" inputlist.
#+					Fill out the "parameters" section according to your needs before running!
##Created by:		Noam Wajnman
##Creation Date:	March 17, 2014
###################################################################################################################
#VARIABLES
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath
$RemoteCertDir = "c:\temp\certs\" #temporary file location on remote server
$logfile = "$dir\logs\" + "cert-bulkimport_" + $(get-date -Format "yyyy-MM-dd_HH-mm-ss") + ".log"
$servers = gc "$dir\servers.txt"
$psexec = "$dir\psexec.exe"
#parameters
$DebugPreference = "continue" #comment out to disable debug info
$certRootStore = "localmachine"
$certStore = "My"
$Cert = "$dir\certs\some_cert.pfx" #certificate to import
$CertPW = "Password" #password for certificate to import
$CertCN = "some.domain.com" #common name of certificate to import
#FUNCTIONS
function log-entry {
    param(
        $message
    )
    $time = Get-date
    Add-Content -Path $logfile -Value "$time`t$message"
}
function RemoteCopy-Certificate {
    param(
        $server
    )
    $copyresult = $false
    $RemoteDir = "\\$server\" + $RemoteCertDir.replace(":","$")	
    Write-Debug "Creating directory $RemoteDir"
    log-entry -message "Creating directory $RemoteDir"	
    New-Item -ItemType "directory" $RemoteDir -Force
    $RemoteCertPath = $RemoteDir + (gci $Cert).Name	
    Write-Debug "Copying $Cert to $RemoteDir"
    log-entry -message "Copying $Cert to $RemoteDir"	
    Copy-Item -Path $Cert -Destination $RemoteCertPath -Force
    if (Test-Path -Path $RemoteCertPath) {
        $CopyResult = $true		
        Write-Debug "Copy of $Cert to $RemoteDir succeeded."
        log-entry -message "Copy of $Cert to $RemoteDir succeeded." 
    }
    else {
        $CopyResult = $false 		
        Write-Debug "Error - Copy of $Cert to $RemoteDir failed."
        log-entry -message "Error - Copy of $Cert to $RemoteDir failed."
    }
    return $CopyResult
}
function Create-ImportScript {
    param(
        $server
    )
    $ScriptCreateResult = $false
    $RemoteScriptPath = "\\$server\" + $RemoteCertDir.Replace(":","$") + "import-pfx.ps1"    
    $CertPath = "'" + $RemoteCertDir + (gci $Cert).Name + "'"    
    $crtPass = "'" + $CertPW + "'"
    $SB = @"
`n
`$crt = new-object System.Security.Cryptography.X509Certificates.X509Certificate2
`$crt.import($certPath,$crtPass,"Exportable,PersistKeySet")
`$store = new-object System.Security.Cryptography.X509Certificates.X509Store(`'$certStore`',`'$certRootStore`') 
`$store.open("MaxAllowed") 
`$store.add(`$crt) 
`$store.close()
"@
    log-entry -message "Creating import script $RemoteScriptPath"
    Write-Debug "Creating import script $RemoteScriptPath`:$SB`n"	
    $SB | Out-File $RemoteScriptPath -force
    if (Test-Path -Path $RemoteScriptPath) {
        $ScriptCreateResult = $true		
        Write-Debug "Creation of $RemoteScriptPath succeeded."
        log-entry -message "Creation of $RemoteScriptPath succeeded." 
    }
    else {
        $ScriptCreateResult = $false		
        Write-Debug "Error - Creation of $RemoteScriptPath failed."
        log-entry -message "Error - Creation of $RemoteScriptPath failed."
    }
    return $ScriptCreateResult    
}
function Invoke-Importscript {
    param(
        $server
    )
    $ImportScriptPath = $RemoteCertDir + "import-pfx.ps1"
    $arg1 = "/acceptEula"
    $arg2 = "\\$server"
    $arg3 = "cmd /c"
    $arg4 = "`"echo .|powershell -file $ImportScriptPath`""	
    Write-Debug "invoking remote import script on $server"   
    log-entry -message "invoking remote import script on $server"	
    Start-Process -Wait -FilePath $psexec -ArgumentList "$arg1 $arg2 $arg3 $arg4"
}
function Remove-TempFiles {
    param(
        $server
    )
    $RemoteScriptDir = "\\$server\" + $RemoteCertDir.Replace(":","$")	
    Write-Debug "Removing temporary cert and script files from $RemoteScriptDir"
    log-entry -message "Removing temporary cert and script files from $RemoteScriptDir"	
    gci -Recurse -Path $RemoteScriptDir | Remove-Item
}
function List-RemoteCerts {
    param(
        $server
    )
    $ro = [System.Security.Cryptography.X509Certificates.OpenFlags]"ReadOnly"
    $lm = [System.Security.Cryptography.X509Certificates.StoreLocation]"LocalMachine"
    $store = new-object System.Security.Cryptography.X509Certificates.X509Store("\\$server\$CertStore", $lm)
    $store.Open($ro)
    return $store.Certificates
}
function Confirm-CertImport {
    param(
        $server
    )
    Write-Debug "Checking if certificate import on $server was successful."
    log-entry -message "Checking if certificate import on $server was successful."	
    $IsCertThere = $false    
    $MyCerts = List-RemoteCerts -server $server        
    $MyCerts | % {	
        Write-Debug "Found certificate: $($_.Subject)"		
        if ($_.Subject -like "*$CertCN*") {		
            Write-Debug "*************Certificate match for $CertCN found!)"			
            $IsCertThere = $true
        }
    }
    return $IsCertThere       
}
#SCRIPT MAIN
new-item -itemtype "file" $logfile -force | out-null #create the log file for this script execution
Write-Debug "Script run started."
log-entry -message "Script run started."
$servers | % { #loop through the servers which we will import the certificate to
    Write-Debug "Starting cert import on $_".ToUpper()
    log-entry "Starting cert import on $_".ToUpper()
    $copyResult = RemoteCopy-Certificate -server $_ # Find out if the copy of the cert to the server was successful
    if ($copyResult) {   #If the copy of the certificate to the server succeeded then proceed.     
        $ScriptCreateResult = Create-ImportScript -server $_ #Find out if the script was created and copied to the server successfully
        if ($ScriptCreateResult) { #if the script was created/copied to the server successfully then proceed.
            Invoke-Importscript -server $_ #import the certificate into the specified store
            Remove-TempFiles -server $_ #Remove temp files such as the cert and import script
            $CertImportResult = Confirm-CertImport -server $_ #check that the cert was imported successfully and write the result to the log.
            if ($CertImportResult) {
                Write-Debug "Success! $CertCN was successfully imported on $_"
                log-entry -message "Success! $CertCN was successfully imported on $_"
            }
            else {
                Write-Debug "Error! Import of $CertCN on $_ failed."
                log-entry -message "Error! Import of $CertCN on $_ failed."
            }
        }
    }
    Write-Debug "-------------------------------------------------------------------------------------------------------------------------"
    log-entry "`n`n"
}

Powershell – Get OS install date from remote servers

Standard

Here’s a handy little script to get the OS install date from your servers and export all the information to a CSV file.
The script is split into three sections variables, functions and script main. The script loops through all servers in the “servers.txt” file (must be placed in the same directory as the script) and gets the OS install date using WMI.

Variables

Below is a short walkthrough of the variables section of the script.

#VARIABLES
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath
$servers = @(gc "$dir\servers.txt")

The first two lines of code –
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath

are really practical as it gives me an object $dir which always points to where the script was run. This makes the script more robust as I can freely move the script around and run it from many locations without having to change any paths in the script.
$servers is an array populated by running Get-Content on the servers.txt file.

Functions

The script uses two functions to do most of the work.
1. Function Get-OSInstallDate

function Get-OSInstallDate {
	param (
		$computerName = $Env:COMPUTERNAME
	)
	[datetime]$OSInstallDate = ([WMI]'').ConvertToDateTime((Get-WmiObject Win32_OperatingSystem -ComputerName $computerName).InstallDate)
	if ($?) {
		return $OSInstallDate
	}
}

This function takes a parameter $computerName and uses the WMI class Win32_OperatingSystem to get the OS install date. The OS install date is also converted so we can return it as a neat datetime object.
2. Function ExportSpecial-CSV

function ExportSpecial-CSV {
	param (
		$inputArray,
		$InfoType		
		)	
    $inputArray | Export-Csv "$dir\$InfoType.csv" -noTypeInformation -Force  
}

This function is extremely simple. It takes an array as input and exports it to CSV in the script directory.

Script Main

$OSInstallDates = @()
$servers | % {
	$date = Get-OSInstallDate -computerName $_
	if ($date) {
		$OSInstallDate = "" | Select "Computer","OSInstallDate"
		$OSInstallDate.Computer = $_
		$OSInstallDate.OSInstallDate = $date
		$OSInstallDates += $OSInstallDate
	}
}
ExportSpecial-CSV -inputArray $OSInstallDates -InfoType "OS_Install_dates"

In the script main I loop through $servers and run the Get-OSInstallDate function to get the info. If the OS install date was retrieved successfully A custom object $OSInstallDate with two properties “Computer” and “OSInstallDate” is created and then added to the array $OSInstallDates. After the foreach loop I use the function ExportSpecial-CSV to export the array $OSInstallDates to the script directory.
That’s it!
Below is the full script. Good luck!

##################################################################################################################
##Script:			Get-OSInstallDate.ps1
##Description: 		Script uses WMI to get the OS install date from the servers listed in "servers.txt" and then 
#+					exports the information to CSV.
##Usage:			Create a file "servers.txt" in the script directory and put in the names of the servers to query.
#+					Run the script and it will create a file called "OS_Install_Dates.csv" in the same directory.
##Created by:		Noam Wajnman
##Creation Date:	February 5, 2014
###################################################################################################################
#FUNCTIONS
function Get-OSInstallDate {
	param (
		$computerName = $Env:COMPUTERNAME
	)
	[datetime]$OSInstallDate = ([WMI]'').ConvertToDateTime((Get-WmiObject Win32_OperatingSystem -ComputerName $computerName).InstallDate)
	if ($?) {
		return $OSInstallDate
	}
}
function ExportSpecial-CSV {
	param (
		$inputArray,
		$InfoType		
		)	
    $inputArray | Export-Csv "$dir\$InfoType.csv" -noTypeINformation -Force  
}
#VARIABLES
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath
$servers = @(gc "$dir\servers.txt")
#SCRIPT MAIN
$OSInstallDates = @()
$servers | % {
	$date = Get-OSInstallDate -computerName $_
	if ($date) {
		$OSInstallDate = "" | Select "Computer","OSInstallDate"
		$OSInstallDate.Computer = $_
		$OSInstallDate.OSInstallDate = $date
		$OSInstallDates += $OSInstallDate
	}
}
ExportSpecial-CSV -inputArray $OSInstallDates -InfoType "OS_Install_dates"