Using PowerShell to delete IIS log files

It’s that time of the month again.

No, not that time. I’m talking about the «Damn! IIS log files are filling up the server again». That time of the month. All sysadmins have been there at some time or another. You’re responsible for some webserver with a fair amount of traffic, and you regularly need to delete the old log files to free up disk space. Finally you decide to try and automate the process, but how to do it? You could use any number of methods to get the job done, I am of course going to focus on using PowerShell.

Note that the following will not work with older versions of IIS than 7.5 (that means earlier than Server 2008 R2)

Where are my log files?

Finding these log files is the first step of our script. The location of IIS log files may vary from site to site. By default they are all placed in several folders in «%SystemDrive%\inetpub\logs\LogFiles».

I was keen to solve this problem for multiple servers and would rather not have to specify the directory of the log files, only the name of the website. The PowerShell code requires a bit of tasty regex to ensure the flexibility we’re after.

Import-Module WebAdministration

$Website = Get-Item IIS:\Sites\$WebsiteName
 $LogFileDirectory = $Website.logfile.directory

if ($LogFileDirectory -match "(%.*%)\\") {
 $LogFileDirectory = $LogFileDirectory -replace "%(.*%)\\","$(cmd /C echo $matches[0])"
 }

Ok, ok! So regex can be a little confusing, what the heck is going on here?

The log file directory is often specified using environment variables such as %SystemDrive%. PowerShell doesn’t understand environment varibles in the old cmd format and therefore we have to translate them using cmd.

In order to get this done automatically without user interaction, the regex will check to see if the log path starts with an environment variable and if so, run echo in cmd and pass the output back to PowerShell. Thereby ensuring paths beginning with %SystemDrive% or %windir% or whatever will not cause our script to fail.

Delete, Delete, Delete!

With our log folder located, it’s time to get our hands dirty. What’s next? Right. Delete those damn log files!

The obvious issue here is making sure we actually only delete the files we want to. We really don’t want to be in the situation of explaining that the reason the website is returning 404 errors is because we deleted the inetpub folder. The code to get the job done is a piece of cake:


$toBeDeleted = Get-ChildItem -Path $LogFileDirectory -Recurse | where {
 $_.LastWriteTime -lt (Get-Date).adddays(-$DaysToRetain) -and $_.PSIsContainer -eq $false
 }

if ($toBeDeleted -ne $null) {
 $ToBeDeleted | % {
 Remove-Item $_.FullName
 }
 }

I don’t feel the need to explain this code in detail – List all files, delete all files. Simples.

Script Parameters

I’ve only added two parameters for this script.

Let’s start off by defining which website we would like to target. Keep in mind that websites often (and by default) share the same log files folder and therefore you may be deleting logs for several sites even though you specify a website. If this is an issue you will need to define separate log file locations for each site before running this script.

How many days of retention do we want? I’m going to default this to 30 days but add the parameter for flexibility. Because of the default value, I’m not even bothering setting this to mandatory.

Putting it all together in code

TL;DR? Here is all the code sewn together in perfect harmony. Perfect harmony does of course mean that you should run this on a test environment before deploying it but you knew that already didn’t you?

Function Clean-IISLogs {
 Param(
 [Parameter(Mandatory=$true)]
 [String]$WebsiteName,

[Parameter(Mandatory=$false)]
 [Int]$DaysToRetain = 30
 )

Import-Module WebAdministration

$Website = Get-Item IIS:\Sites\$WebsiteName
 $LogFileDirectory = $Website.logfile.directory

if ($LogFileDirectory -match "(%.*%)\\") {
 $LogFileDirectory = $LogFileDirectory -replace "%(.*%)\\","$(cmd /C echo $matches[0])"
 }

$toBeDeleted = Get-ChildItem -Path $LogFileDirectory -Recurse | where {
 $_.LastWriteTime -lt (Get-Date).adddays(-$DaysToRetain) -and $_.PSIsContainer -eq $false
 }


if ($toBeDeleted -ne $null) {
 $ToBeDeleted | % {
 Remove-Item $_.FullName
 }
 }

}

And there you have it. Now the only thing between you and an IIS server that doesn’t require you to clean old log files is your task scheduler.

Using WMI with PowerShell

If you aren’t already using WMI to retrieve information from computers in your domain, it’s time to get started. WMI is a goldmine of information and it’s never been easier to access.

I’ll be covering WMI in more detail later as there is so much to go through. For now I’ll demonstrate a short script that can reveal how easy it is to generate useful reports when accessing WMI through PowerShell.

This function will take an array of computer names, attempt to query each of their WMI databases, retrieve the free space available and also calculate the free space percentage. The function accepts pipelining. I have not added any error handling which means if a machine is not configured to let you query its WMI database, you will get a big red error telling you so. Don’t worry though, you can do no damage with the function as it does not set any values. Here is an example of usage and output:

And here is the source

Function Get-FreeSpace {
param(
[Parameter(ValueFromPipeline=$true)]
[String[]]$Computers
 )

 Begin {
 $Result = @()

 }

 Process {
 $Drives = Get-WmiObject 'Win32_LogicalDisk' -ComputerName $Computers | where { $_.DriveType -eq 3 }

 $Drives | % {
 $newobj = New-Object System.Object
 $newobj | Add-Member -Name Server -MemberType NoteProperty -Value $_.__Server
 $newobj | Add-Member -Name DriveName -MemberType NoteProperty -Value $_.Name
 $newobj | Add-Member -Name FreeSpace -MemberType NoteProperty -Value ("{0:0.00}GB" -f ($_.FreeSpace / 1GB))
 $newobj | Add-Member -Name Size -MemberType NoteProperty -Value ("{0:0.00}GB" -f ($_.Size / 1GB))
 $newobj | Add-Member -Name PercentFree -MemberType NoteProperty -Value ("{0:0.00}%" -f ($_.FreeSpace * 100 / $_.Size))
 $Result += $newobj
 }
 }

 End {
 return $Result
 }

}

Allow Dial-in and assign static IP Addresses in Active Directory with PowerShell

So it turns out setting IP addresses in Active Directory with PowerShell is not quite as straight forward as you might expect. It is perfectly possible though, lets have a look at how it works.

Just so we are on the same page, this is what we are looking at – our Dial-in tab in AD:

By default, Network Access Permission is set to “Control access through NPS Network Policy” and there is no assigned IP address.

First let’s take a look at which attributes are affected:


These attributes are set when we set the Dial-in tab as shown in the caption above.

The first thing you might be wondering is what on earth 169090600 is. It is in fact our IP address 10.20.30.40 as an Integer representation of the 32bit binary that is the IP address.
That is 10.20.30.40 = 00001010.00010100.00011110.00101000.
Remove the dots and you get 00001010000101000001111000101000 = 169090600.

Now that we know what we are dealing with, let’s get to the PowerShell part of it.
The first thing we have to do is convert our IP from 10.20.30.40 to 169090600. I wrote a quick function to do just that.


function Convert-IP {
 param(
 [Parameter(ValueFromPipeline=$true)]
 [String]$IPAddress = "127.0.0.1"
 )

 $octetsDecimal = $IPAddress -split "\."
 $octetsBinary = $octetsDecimal | % { [convert]::ToString($_, 2) }

$octetsBinary = $octetsBinary | % { ("0" * (8 - $_.length)) + $_ }

$octetsBinary | % { $binaryIP += $_ }
 $integerIP = [convert]::ToInt32($binaryIP, 2)

 return $integerIP
}

Setting the attributes in AD is simple

Set-QADUser -Identity "Testuser" -IncludedProperties @("msNPAllowDialin", "msRADIUSFramedIPAddress", "msRASSavedFramedIPAddress") -ObjectAttributes
@{msNPAllowDialin=$true;msRADIUSFramedIPAddress=169090600;msRASSavedFramedIPAddress=169090600}

Combine the two and you can make this task painless

$IP = Convert-IP "10.20.30.40"
Set-QADUser -Identity "Testuser" -IncludedProperties @("msNPAllowDialin", "msRADIUSFramedIPAddress", "msRASSavedFramedIPAddress") -ObjectAttributes @{msNPAllowDialin=$true;msRADIUSFramedIPAddress=$IP;msRASSavedFramedIPAddress=$IP}