#v1.0 - full release #v0.2 - Added user questions #v0.1 - First draft # Written by: Martin Howlett martin.howlett@rackspace.co.uk #Function to change a function name (like Get-SophosVersion) to a column name to use in the PSobject (Sophos Version) Function Get-NameWithSpaces{ Param ( [Parameter()]$Functioname ) #Get the name of the function and split to remove the PowerShell verb $FullFunctionName = $Functioname.Split("-")[1] #Get the name of the function and split it by capital letters $splitname = ("$FullFunctionName" -creplace '[A-Z]', ' $&').Trim().Split($null) #Rejoin the function name adding spaces for a nice looking column header $NameWithSpaces = $splitname -join " " Return $NameWithSpaces } Function Get-FunctionMetaData{ Param ( [Parameter()]$Functions ) $ScriptList = @() #Get the meta data from each script ForEach ($Function in $Functions){ #Blank our variables as this was causing duplicates if template not filled in correctly $ColumnName = $null $Supported = $null $keywords = $null $Prerequisites = $null $MakesChanges = $null $ScriptTimeout = $null $DownloadFile = $null $Collection = $null $GroupFunctions = $null $SortOrder = $null Import-Module $Function.FullName -Force -DisableNameChecking $FunctionName = $Function.Name $FunctionName = $FunctionName.Replace(".ps1","") $FunctionHelp = Get-Help $FunctionName -Full #Get the Synopsis, this is going to be our description $description = $FunctionHelp.synopsis $name = $FunctionHelp.name If ($FunctionHelp.parameters){ $scriptinput = $true }ELSE{ $scriptinput = $false } $TextStream = $FunctionHelp.Description | out-string -Stream $SortOrder = @() $GroupFunctions = @() $Sort = $false $Group = $true ForEach ($line in $TextStream){ #loop through the description and find the line with the meta data IF ($line -like "WHAM - supported:*"){[string]$templine = $line ; $Supported = $templine.Replace("WHAM - supported: ","")} IF ($line -like "WHAM - keywords:*"){[string]$templine = $line ; $keywords = $templine.Replace("WHAM - keywords: ","")} IF ($line -like "WHAM - Prerequisites:*"){[string]$templine = $line ; $Prerequisites = $templine.Replace("WHAM - Prerequisites: ","")} IF ($line -like "WHAM - Makes changes:*"){[string]$templine = $line ; $MakesChanges = $templine.Replace("WHAM - Makes changes: ","")} IF ($line -like "WHAM - Column Header:*"){[string]$templine = $line ; $ColumnName = $templine.Replace("WHAM - Column Header: ","")} IF ($line -like "WHAM - Script time out (min):*"){[string]$templine = $line ; $ScriptTimeout = $templine.Replace("WHAM - Script time out (min): ","")} IF ($line -like "WHAM - Download File:*"){[string]$templine = $line ; $DownloadFile = $templine.Replace("WHAM - Download File: ","")} IF ($line -like "WHAM - Collection:*"){[string]$templine = $line ; $Collection = $templine.Replace("WHAM - Collection: ","")} If ($Collection -like "true*"){$Collection = $true} #in the first line "WHAM - Group start:" we set this to true so next time we loop round, we are going to have the list If ($GroupStart -eq $true){ #this marks when to stop If ($line -like "*WHAM - Group end*"){$GroupStart = $false ; continue} #If we find a matching line, we trim it and add to array [string]$trimmed = $line $GroupFunctions += $trimmed.Trim() } #in the first line "WHAM - Sort order:" we set this to true so next time we loop round, we are going to have the list If ($Sort -eq $true){ #this marks when to stop If ($line -like "*WHAM - Sort order end*"){$Sort = $false ; continue} #If we find a matching line, we trim it and add to array [string]$trimmed = $line $SortOrder += $trimmed.Trim() } IF ($line -like "WHAM - Sort order:*"){$Sort = $true} IF ($line -like "WHAM - Group start*"){$GroupStart = $true} } If(-NOT $ColumnName){ Write-host "WARNING! Could not read in column name for $($function.name). Skipping..." -ForegroundColor Red continue } #If the line isnt in the template just set sort order to none IF ($SortOrder.Length -lt 1){ $SortOrder = "None" } #Create a PSObject of the metadata we scraped from the script, and save it to our array $ScriptTemp = New-Object PSObject -Property @{ Name = $Name Description = $Description Keywords = $Keywords Prerequisites = $Prerequisites MakesChanges = $MakesChanges ScriptInput = $ScriptInput FullName = $Name SortOrder = $SortOrder ColumnName = $ColumnName ScriptTimeout = $ScriptTimeout DownloadFile = $DownloadFile Collection = $Collection GroupFunctions = $GroupFunctions } $ScriptList += $ScriptTemp } return $ScriptList } Function Get-UserSelectedFunction{ Param ( [Parameter()]$Functionlist ) #Sort the entries so they are organised with the groups first $Functionlist = $Functionlist | Sort-Object Name $x = 0 $ReturnedFunctions = "" $title = "Please select templates(s)...." #Open Gridview with our script metadata so the user can pick some $UserFunctions = $Functionlist | Select-Object Name,Description,MakesChanges,Prerequisites,Keywords,ScriptInput | Sort-Object Name | Out-GridView -PassThru -Title $title #if we return more then 2 groups, ask the user again as we can only one on 1 group at a time. this should really be a loop but this will do for now $GroupCount = $ReturnedFunctions | where-object {$_.Name -like "_Group*"} If (($GroupCount | Measure-Object).count -gt 1) { write-host "Warning, you can only select one group at a time. Please select again" -ForegroundColor Yellow $ReturnedFunctions = $Functionlist | Select-Object Name,Description,MakesChanges,Prerequisites,Keywords,ScriptInput | Sort-Object Name | Out-GridView -PassThru -Title $title } #Filter our functions to just the ones the users selected $ReturnedFunctions = $Functionlist | Where-Object {$UserFunctions.Name -Contains $_.Name} Return $ReturnedFunctions } Function Get-UserFunctionUserInput { Param ( [Parameter(mandatory=$true)]$Functions, [Parameter(mandatory=$true)]$TemplatePath ) #Check if any scripts require any user input $Questionlist = @() ForEach ($function in $Functions) { Write-Verbose "line $($MyInvocation.ScriptLineNumber)" #Current line number Write-Verbose "script.ScriptInput $($script.ScriptInput)" #Check the script input variable If ($function.ScriptInput) { #If its true, we get the help parameters from the script Import-Module "$TemplatePath\$($function.name).ps1" -DisableNameChecking -Force #this splits the script params into a nice array $FunctionParams = (Get-Help $function.Name -Full).parameters $FunctionParams = $FunctionParams[0].parameter #loop through the params ForEach ($Parameter in $FunctionParams) { #If the param name is force, we now this is a switch so we can just add it If ($Parameter.Name -eq "force") { $TempAnswer = New-Object PSObject -Property @{ Script = $Function.name Value = $true ParameterName = $Parameter.Name } $Questionlist += $TempAnswer } ELSE { #convert the param description to a string $ParamString = $Parameter.Description | Out-String -Stream #loop through the lines in the string array and get the description ForEach ($line in $ParamString) { If ($line -like "WHAM Prompt: *") { [string]$templine = $Line $QuestionText = $templine.Replace("WHAM Prompt: ","") } } $Answer = Read-Host $QuestionText $TempAnswer = New-Object PSObject -Property @{ Script = $Function.name Value = $Answer ParameterName = $Parameter.Name } $Questionlist += $TempAnswer } } } } Return $Questionlist } Function Write-SelectedFunctions { Param ( [Parameter()]$ReturnedFunctions ) write-host "`nYou have selected the following function templates: " write-host " " ForEach ($function in $($ReturnedFunctions)) { write-host "$function" -ForegroundColor Green } } Function Get-FunctionContent { Param ( [Parameter(mandatory=$true)]$Functions, [Parameter(mandatory=$true)]$TemplatePath ) #now we have our scripts, we need to create the script block $FunctionCode = @() ForEach ($function in $ReturnedFunctions) { #Reset the $read param to false $read = $false #import the code from the template $FunctionContent = Get-Content "$TemplatePath\$($function.Name).ps1" ForEach ($Line in $FunctionContent) { #When we find the start of the function start reading it in If ($Line -like "Function*") { $Read = $true } if ($Read) { $FunctionCode += $line } } } Return $FunctionCode } Function Get-WHAMFunctionCommands { Param ( [Parameter()]$Functions, [Parameter()]$Questionlist ) #Create the commands to add the info the psobject on the local machine $Commands = @() ForEach ($Function in $Functions) { $Params = $null #If the script has input If ($Function.ScriptInput -eq "Yes") { #The script has input so edit the function call to add the params ForEach ($Answer in $Questionlist) { If ($Answer.Script -eq $Function.Name) { #build out the command line with the params If ($Answer.ParameterName -eq "force") { $Params = "$Params -$($Answer.ParameterName)" } ELSE { $Params = "$Params -$($Answer.ParameterName) `"$($Answer.Value)`"" } } } #$ColumnName = Get-NameWithSpaces -Functioname $($Function.Name) #build the command with the commands from the questions $Commands += "`"$($Function.ColumnName)`" = `$($($Function.Name) $Params)" } ELSE { $ColumnName = Get-NameWithSpaces -Functioname $($Function.Name) #the script doesnt have input, so it will build normally $Commands += "`"$($Function.ColumnName)`" = $($Function.Name)" } } Return $Commands } Function Get-WHAMScriptblock { Param ( [Parameter()]$FunctionCode, [Parameter()]$Commands, [Parameter()]$Username, [Parameter()]$FunctionNames, [Parameter()]$CustomScriptBlock ) #Join the commands and add a `n, which will put them all on the new line in the event log If($CustomScriptBlock){ $Functions = "Custom scriptblock" }else{ $Functions = "" ForEach ($Function in $FunctionNames){ $Functions = "$Functions``n$Function" } } #Build the script block using a herestring $scriptblock1 = @" Param( [Parameter()]`$DomainUser, [Parameter()]`$DomainPassword ) Try { #Set our domain user and password variables to global so we can use them in functions `$Global:DomainUser = `$DomainUser `$Global:DomainPassword = `$DomainPassword IF (-NOT ([bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).groups -match "S-1-5-32-544"))) { return "UAC causing script to not run as admin" ; break } #Event logging - modified from function written by Bob Larkin `$Logname = "Application" `$Source = "RS PowerShell Script" `$RSEventLog = [System.Diagnostics.EventLog]::SourceExists(`$Source) `$RSEventID = 101 `$EventMessage = "The follow Powershell functions are being run on this server by $username :``n$Functions" if (-not `$RSEventLog){ New-EventLog -LogName `$Logname –Source `$Source } Write-EventLog –LogName `$Logname –Source `$Source –EntryType Information –EventID `$RSEventID –Message `$EventMessage #End of event logging "@ If($CustomScriptBlock){ $scriptblock2 = $CustomScriptBlock }else{ $scriptblock2 = @" $($FunctionCode | Out-String) `$Output = New-Object PSObject -Property @{ `"Server`" = `"`$env:COMPUTERNAME`" $($Commands | Out-String) } #End of scriptblock2 "@ } $scriptblock3 = @" `$output | Export-clixml -Path "C:\rs-pkgs\wham_export.xml" -Force; `$Test = Test-Path -path "C:\rs-pkgs\wham_export.xml"; `$RSEventID = 102 Write-EventLog –LogName `$Logname –Source `$Source –EntryType Information –EventID `$RSEventID –Message "Script execution has finished" IF(`$Test -eq `$true){Return "Script OK"}ELSE{Return "Error: XML File not found on `$ENV:ComputerName"} } Catch { Return "Error: Script had unhandled error on `$ENV:ComputerName" } "@ #Convert it to a script block $scriptBlock = [Scriptblock]::Create("$scriptblock1`r`n$scriptblock2`r`n$scriptblock3") return $scriptBlock } #NO LONGER USED Function Get-WHAMScriptGroups { $Groups = @() $QCGroup = New-Object PSObject -Property @{ Name = "_Group-QC" MakesChanges = "No" Description= "" FullName = "_Group-QCs" ScriptInput = "Group:QC" Prerequisites = "No" Keywords = "group" } $GRoups += $QCGroup Return $Groups } Function Get-WHAMGroupTemplates { Param ( [Parameter()]$ReturnedFunctions ) IF ($ReturnedFunctions.Name -like "_Group*") { $SearchTerm = $ReturnedFunctions.Name $SearchTerm = $SearchTerm.Split("-")[1] $ReturnedFunctions = $Functionlist | Where-Object {$_.Keywords -like "*$SearchTerm*"} } Return $ReturnedFunctions } Function New-ScriptFile { Param ( [Parameter(Mandatory=$true)]$LocalScriptPath, [Parameter(Mandatory=$true)]$Scriptblock ) #Remove the powershell filesystem type string as it can break stuff $LocalScriptPath = $LocalScriptPath.Replace("Microsoft.PowerShell.Core\FileSystem::","") #Create the file New-Item -Path $LocalScriptPath -ItemType File -Force | Out-Null #Confirm file exists If (-NOT (Test-Path $LocalScriptPath)) { write-host "failed to create PS1 file" break } Set-Content -Path $LocalScriptPath -Value $Scriptblock } Function Get-ScriptTimeOut { Param ( [Parameter(Mandatory=$true)]$Functions ) $Timeout = $null #add all the timeouts together Foreach ($function in $Functions) { #only add a timeout if its bigger then 1 and present If (($function.ScriptTimeout -gt 1) -AND ($function.ScriptTimeout)) { [int64]$seconds = $function.ScriptTimeout #timeout on the template is in minutes, so times by 60 to get seconds and add it to our total $Timeout = $Timeout + ($seconds * 60) } } #if its blank set the default to 60 seconds If (-NOT $Timeout) { $timeout = 60 } return $timeout } Function Get-TemplatePath{ Param ( [Parameter()][switch]$QC, [Parameter()][switch]$Dev ) Write-Verbose "QC: $QC Dev: $Dev" IF ($QC){ $TemplatePath = "D:\DFS-Public\WHAM\Templates\QC" } elseif ($Dev){ $TemplatePath = "D:\DFS-Public\WHAM\Templates\Dev" }else{ $TemplatePath = "D:\DFS-Public\WHAM\Templates\Live" } Return $TemplatePath } Function Get-WHAMScriptTemplate { Param ( [Parameter()][switch]$QC, [Parameter()][switch]$Dev, [Parameter()]$TemplateName ) Write-Verbose "QC: $QC Dev: $Dev" Write-Verbose "TemplateName: $TemplateName" $Params = @{} If ($QC){$Params.Add("QC", $true)} If ($Dev){$Params.Add("Dev", $true)} $TemplatePath = Get-TemplatePath @params #Set our collection var to false $Collection = $false #import the script files $Functions = Get-ChildItem $TemplatePath | Where-Object {($_.Extension -eq ".ps1")} #Call the function to parse the functions to get the meta data $Functionlist = Get-FunctionMetaData -Functions $Functions #If the user has specfied the template names, use them If ($TemplateName){ $ReturnedFunctions = @() $TemplateName = $TemplateName -split "," ForEach ($template in $TemplateName){ $ReturnedFunctions += $Functionlist | Where-Object {$_.Name -like $template} } } else{ Write-Host "Please select a Powershell function / group of functions..." $ReturnedFunctions = Get-UserSelectedFunction -functionlist $Functionlist } If (-NOT $ReturnedFunctions) { write-host "No template selected, quitting!" -ForegroundColor red break } If ($ReturnedFunctions.Name -like "*_Group*") { #Save our sort order and group names $SortOrder = $ReturnedFunctions.SortOrder $GroupName = $ReturnedFunctions.Name #If we selected our groups, lets filter by the group function list $GroupFunctionList = $ReturnedFunctions.GroupFunctions $ReturnedFunctions = $Functionlist | where-object {$GroupFunctionList -Contains $_.Name} } else { $SortOrder = $false } #this is the old method, so we comment this out for now #$ReturnedFunctions = Get-WHAMGroupTemplates -ReturnedFunctions $ReturnedFunctions $QuestionList = Get-UserFunctionUserInput -Functions $ReturnedFunctions -TemplatePath $TemplatePath $FunctionCode = Get-FunctionContent -Functions $ReturnedFunctions -TemplatePath $TemplatePath $Commands = Get-WHAMFunctionCommands -Functions $ReturnedFunctions -Questionlist $Questionlist $scriptBlock = Get-WHAMScriptblock -FunctionCode $FunctionCode -Commands $Commands -Username $ENV:USERNAME -FunctionNames $ReturnedFunctions.Name $ScriptTimeout = Get-ScriptTimeOut -Functions $ReturnedFunctions #this is used by the function section in appstats at the end of the WHAM controller script, the API doesnt like a weird array. We also add group in there so we know a group was used IF ($GroupName) { $FunctionSingleString = $GroupName } else { $FunctionSingleString = $ReturnedFunctions.Name -join "," } #Check our collection array, if we have anything at it just set it to true so we can use it as a switch If (($ReturnedFunctions | Where-Object {$_.Collection -eq $true}| Measure-Object).Count -gt 0) { $Collection = $true } $Output = New-Object PSObject -Property @{ scriptBlock = $scriptBlock FunctionNames = $ReturnedFunctions.Name SortOrder = $SortOrder FunctionSingleString = $FunctionSingleString ScriptTimeout = $ScriptTimeout DownloadFile = $ReturnedFunctions.DownloadFile Collection = $Collection } Return $Output }