fredag den 30. maj 2014

System Center Service Manager Automation

I’ve never worked with SCSM before, but a few weeks back I was asked to implement some automation processes using Microsoft Orchestrator (also a new product for me).

One of the jobs needed to add a user to a group, and you would expect something so simple would be easy. But after reading this guide, I ended up with this

image

Are you freaking kidding me ? My life is just to short for that, so after a while I ended up with this instead

image

The main problem lies in the fact that you need to fetch a lot of related objects. You start by sending the GUID of the Automation job inside SCSM. You then get the related objects, and finally you fetch the real AD objects. The job will of course fail if the user is already member of the group, so feel free to add 5-10 more steps for check group members ships too. Gaaaahhh

So lets look at the PowerShell script instead. First a little back ground. To run PowerShell scripts you need to install a Management pack. I choose Orchestrator Integration Pack for PowerShell Script Execution . Next we need a way to “talk” with SCSM from PowerShell, and I choose SMLETS for that. You need to be aware you can run into some issues with “double hob” when running the PowerShell scripts, so I choose to configure an service account and configure the PowerShell scripts to run on the SCSM server, and the use CredSSP as authentication scheme.

image

And we of course need to pass the GUID of the automation job in SCSM to the PowerShell script

image

And now to the good part. I’ve attached 2 scripts like the “normal” examples you find while goggling. “Add self to group” and “add user to group”. The first assumes an AD Group object has been associated with the Service Request. The latter, assumes an AD User Object and AD Group Object has been associated with the Service Request. You could associate the objects with either the Automation Request or Service Request, doesn’t really matter, the script is easy to modify for either.

As an added bonus you will also find some scripts for handling office 365 subscripts, users, licenses and management of SharePoint Online Service users in a federated environment. And notes about how to call Orchestrator run books from PowerShell completely dynamically with parameters.
(download link)

[CmdLetBinding(DefaultParameterSetName="None")]
param (
[Parameter(Mandatory=$true)][string]$ActivityGuid
)

$logscope = "AddUserToGroup"
$logname = "log"
. 'C:\Runbook\ServiceManager\functions\Initialization.ps1'
WriteHost "ActivityGuid: $ActivityGuid" -class $logscope

# Get current RunBook Activity in Service Manager
$runbook = Get-SCSMObject $ActivityGuid

# Dynamicly find related object (should only be one, of type Service Request )
$serviceRequest = (Get-SCSMRelationshipObject -ByTarget $runbook).SourceObject

$relationClass = Get-SCSMRelationshipClass System.WorkItemRelatesToConfigItem$
$scsmobjects = Get-SCSMRelatedObject -SMObject $ServiceRequest -Relationship $relationClass
$aduser = $null;$adgroup = $null;
foreach($scsmobject in $scsmobjects)
{
$adobject = Get-ADObject $scsmobject.DistinguishedName
if($adobject.ObjectClass -eq 'user')
{
$aduser = Get-ADUser $scsmobject.DistinguishedName
WriteHost "Found user in ad as $($aduser.Name) $($aduser.UserPrincipalName)" -class $logscope
} elseif($adobject.ObjectClass -eq 'group') {
$adgroup = Get-ADGroup $scsmobject.DistinguishedName
WriteHost "Found group in ad as $($adgroup.Name) $($adgroup.UserPrincipalName)" -class $logscope
}
}

if($aduser -and $adgroup)
{
WriteHost "Ensuring $($aduser.UserPrincipalName) is a member of '$($adgroup.Name)'" -class $logscope
# and now. The moment we have all been wating for .. Tadaaaaa, drum roll, blow the trompets, scream hale-juja, and add the user to the group!
Add-ADGroupMember -Identity $adgroup -Members $aduser
} else {
WriteHost "Failed locating user or group in ad!" -class $logscope
Throw "Failed locating user or group in ad!"
}

WriteHost "--Completed--" -class $logscope

XenApp hotfix management

One thing I never understood is how to figure out what patches are needed on a XenApp server. And it has always annoyed me with the whole process of needing to download everything and never knowing what fixes need a reboot.

So a few days ago I started searching for something smarter. Here someone made a script to get the patches but it only works with newer versions of PowerShell, and here someone made a complete solution. but, I like the ability to get status back and it doesn’t seem to check if a patch is actually needed. So I combined the two, into one PowerShell script that also works in PowerShell 2.0.
( download link )

Set-Location 'c:\'
$snapin = Get-PSSnapin | where {$_.name -eq 'Citrix.Common.Commands'}
if($snapin -eq $null){ Add-PSSnapin Citrix.Common.Commands }
$snapin = Get-PSSnapin | where {$_.name -eq 'Citrix.XenApp.Commands'}
if($snapin -eq $null){ Add-PSSnapin Citrix.XenApp.Commands }

function installXenappHotfix([string]$HotfixName, [string]$hotfixpath ){
$exitcode = (Start-Process -FilePath "msiexec.exe" -ArgumentList "/qb /passive /norestart /p `"$hotfixpath`"" -Wait -Passthru).ExitCode
if($exitcode -eq 3010){
$exitcode = 1604
#Write-Host 'Restart needed, so restarting. doh!'
#restart-computer -force -throttlelimit 10; exit $exitcode
#exit $exitcode
return $exitcode
}
if($exitcode -ne 0){
Write-Host ('Unknown exitcode: ' + $exitcode)
#exit $exitcode
return $exitcode
}
return 0
}


function installXenappHotfixWithIsNeededCheck([string]$HotfixName, [string]$hotfixpath ){
# Get current computername and XenServer object
$computername = $env:computername

$isInstalled = $false;
try {
foreach($hotfix in (Get-XAServerHotFix -ServerName $computername)){
if($hotfix.HotfixName -eq $HotfixName){ $isInstalled = $true; }
}
# IMA service proberly not running.
} catch {
if($_.invocationinfo) {
status ($_.Exception.Message + "`n `n" + $_.InvocationInfo.PositionMessage)
} else {
status $_.ToString()
}
return 1604
}
if($isInstalled -eq $false){
Write-Host ('Missing ' + $HotfixName)
$exitcode = (Start-Process -FilePath "msiexec.exe" -ArgumentList "/qb /passive /norestart /p `"$hotfixpath`"" -Wait -Passthru).ExitCode
if($exitcode -eq 3010){
$exitcode = 1604
Write-Host 'Restart needed, so restarting. doh!'
#restart-computer -force -throttlelimit 10; exit $exitcode
#exit $exitcode
return $exitcode
}
if($exitcode -ne 0){
Write-Host ('Unknown exitcode: ' + $exitcode)
#exit $exitcode
return $exitcode
}
} else {
# Write-Host ($HotfixName + ' ok')
return 0
}
}

function Get-URLContent ($url) {
$client = new-object System.Net.WebClient
#$client.DownloadFile( $url, [IO.Path]::GetTempFileName() )
return $client.downloadString($url)
}
function Get-URLFile ($url, $saveas) {
$client = new-object System.Net.WebClient
#$client.DownloadFile( $url, [IO.Path]::GetTempFileName() )
$client.DownloadFile( $url, $saveas )
}

function CheckXenappHotfix() {
# XenApp 6.5 : $url = "http://support.citrix.com/product/xa/v6.5_2008r2/hotfix/general/?rss=on"
# XenApp 6.0 : $url = "http://support.citrix.com/product/xa/v6.0_2008r2/hotfix/general/?rss=on"
# XenApp 5.0 : $url = "http://support.citrix.com/product/xa/v5.0_2008/hotfix/general/?rss=on"

# PVS 6.1 : $url = "http://support.citrix.com/product/provsvr/pvsv6.1/hotfix/general/?rss=on"
}

$hotfixurl = 'http://support.citrix.com/product/xa/v6.0_2008r2/hotfix/general/?rss=on'
$source = Join-Path (Get-Location).Path 'xa_hotfix'
if(! (Test-Path $source)) { New-Item -ItemType directory -Path $source | Out-Null }

$service = Get-Service imaservice
if($service.Status.ToString() -eq 'Stopped') {
Get-Service imaservice | Start-Service
}
try {
$computername = $env:computername
$installed = (Get-XAServerHotFix -ServerName $computername)
} catch {
if($_.invocationinfo) {
write-error ($_.Exception.Message + "`n `n" + $_.InvocationInfo.PositionMessage)
} else {
write-error $_.ToString()
}
}

write-progress -id 1 -Activity "Xenapp Hotfix Script" -Status "Fetch lastest xenapp hotfixes"
write-progress -id 2 -ParentId 1 -Activity "Xenapp Hotfix Script" -Status "Fetch $hotfixurl"

$hotfixes = @()
$xml = [xml](Get-URLContent $hotfixurl)
foreach($fix in $xml.rdf.item) {
write-progress -id 2 -ParentId 1 -Activity "Xenapp Hotfix Script" -Status "Fetch $($fix.link)"

$html = Get-URLContent $fix.link
$pattern = '(?<=.)/servlet/KbServlet/download/.+?(?=")'
$results = ($html | Select-String -Pattern $Pattern -AllMatches).Matches | select value
if($results) {
if($results -is [system.array]){ $results = $results[0] }
$url = ('http://support.citrix.com' + $results.Value)
$filename = [string][io.path]::GetFileName($url)
if($filename.Contains('.msp')) {
$HotfixName = [string][io.path]::GetFileNameWithoutExtension($filename)
if(! ($installed | ?{$_.HotfixName -eq $HotfixName})) {
$saveas = (Join-Path $source $filename)
if(! (Test-Path $saveas)) {
write-progress -id 2 -ParentId 1 -Activity "Xenapp Hotfix Script" -Status "Downloading $filename"
write-verbose "Downloading $filename"
Get-URLFile $url $saveas
} else {
write-progress -id 2 -ParentId 1 -Activity "Xenapp Hotfix Script" -Status "$filename has allready been downloaded"
write-verbose "$filename has allready been downloaded"
}
$hotfixes += $saveas
} else {
write-progress -id 2 -ParentId 1 -Activity "Xenapp Hotfix Script" -Status "$filename has allready been installed"
write-verbose "$filename has allready been installed"
}
} else {
write-progress -id 2 -ParentId 1 -Activity "Xenapp Hotfix Script" -Status "skipping $filename"
write-verbose "skipping $filename"
}
}
}

$needreboot = $false
$updateSystemInfo = new-object -com Microsoft.Update.SystemInfo
if($updateSystemInfo.rebootRequired){ $needreboot = $true }

write-progress -id 1 -Activity "Xenapp Hotfix Script" -Status "Installing missing xenapp hotfixes"
foreach($filename in $hotfixes) {
$HotfixName = [string][io.path]::GetFileNameWithoutExtension($filename)
write-progress -id 2 -ParentId 1 -Activity "Xenapp Hotfix Script" -Status "Installing $HotfixName"

$exitcode = installXenappHotfix $HotfixName $filename
# ERROR_SUCCESS_REBOOT_REQUIRED
if($exitcode -eq 3010){ $needreboot = $true; }
if($exitcode -eq 1642){
# some hotfixes will shut down the IMA service
# If you then start the service without rebooting it will not report the patch installed
# if that is NOT the case, go do some troubleshootnig to figure out what is wrong

Write-Warning "$HotfixName has allready been installed (ERROR_PATCH_TARGET_NOT_FOUND)"
$needreboot = $true
}
}

write-progress -id 2 -ParentId 1 -Activity "Xenapp Hotfix Script" -Status "n/a" -Completed
if($needreboot) {
write-progress -id 2 -ParentId 1 -Activity "Updating" -Status "Rebooting system"
Write-Warning "Reboot required"
Restart-Computer -Force
}

Windows Update with PowerShell

I’ve had a PowerShell script (rewritten from VBS) for a long time. It works and all, but once in a while when your testing stuff on fresh machines it would drive me nuts I couldn’t get a “status” like when your doing it though GUI.

Doing a progress bar while downloading is/was easy enough, but we all know it’s the installation that takes time, and that gave me a few issues. I finally figured out, I couldn’t re-use the installer, and sometimes it fails if you assign each update without first adding it to an “UpdateColl”. So, here it is (download link)

[CmdletBinding()]
Param
(
[switch]$skipReboot
)
Begin {
$Install = $true;
$EulaAccept = $true;

write-progress -id 1 -Activity "Windows Update" -Status "Initialising"
$UpdateSession = New-Object -ComObject 'Microsoft.Update.Session'
$UpdateSession.ClientApplicationID = 'Wingu Update Client'
}
Process {
write-progress -id 1 -Activity "Windows Update" -Status "Checking for available updates"
$UpdateSearcher = $UpdateSession.CreateUpdateSearcher()
$SearchResult = $UpdateSearcher.Search("IsInstalled=0 and Type='Software'") # and IsHidden=0
$UpdatesToDownload = New-Object -com Microsoft.Update.UpdateColl
foreach ($Update in $SearchResult.Updates)
{
[bool]$addThisUpdate = $false
if ($Update.InstallationBehavior.CanRequestUserInput)
{
#Write-Verbose "> Skipping: $($Update.Title) because it requires user input"
[bool]$addThisUpdate = $true
} else {
if (!($Update.EulaAccepted)) {
Write-Verbose "> Note: $($Update.Title) has a license agreement that must be accepted:"
$Update.AcceptEula() | Out-Null
[bool]$addThisUpdate = $true
} else {
[bool]$addThisUpdate = $true
}
}
if ([bool]$addThisUpdate) {
Write-Verbose "Adding: $($Update.Title)"
$UpdatesToDownload.Add($Update) | Out-Null
}
}
if ($UpdatesToDownload.Count -eq 0) {
Write-Verbose 'All applicable updates were skipped.'
return $false
}
$downloader = $updateSession.CreateUpdateDownloader()
foreach($update in $UpdatesToDownload) {
$counter += 1
write-progress -id 1 -Activity "Updating" -Status "Downloading $counter/$($UpdatesToDownload.count) updates"
if(!$update.IsDownloaded) {
write-progress -id 2 -ParentId 1 -Activity "Updating" -Status "Downloading $($update.Title) $([int]($update.MaxDownloadSize / 1MB))MB"
$updateCollection=New-Object -com Microsoft.Update.UpdateColl
$updateCollection.Add($Update) | Out-Null
$downloader.Updates = $updateCollection
$Result = $downloader.Download()
} else {
write-progress -id 2 -ParentId 1 -Activity "Updating" -Status "isready $($update.Title)"
}
}


$needreboot = $false
$updateSystemInfo = new-object -com Microsoft.Update.SystemInfo
if($updateSystemInfo.rebootRequired){ $needreboot = $true }
$counter = 0
foreach($update in $UpdatesToDownload) {
$counter += 1
write-progress -id 1 -Activity "Updating" -Status "Installing $counter/$($UpdatesToDownload.count) updates"
write-progress -id 2 -ParentId 1 -Activity "Updating" -Status "Installing $($update.Title)"
$updatesToInstall = New-object -com "Microsoft.Update.UpdateColl"
$updatesToInstall.Add($update) | out-null
$installer = $updateSession.CreateUpdateInstaller()
$updateCollection=New-Object -com Microsoft.Update.UpdateColl
$updateCollection.Add($Update) | Out-Null
if($installer.ForceQuiet -eq $false) { $installer.ForceQuiet=$true }
$installer.Updates = $updateCollection
$installationResult = $installer.Install()
if($installationResult.rebootRequired){ $needreboot = $true }
if($installationResult.HResult -eq 4) {
#$resultcode= @{0="Not Started"; 1="In Progress"; 2="Succeeded"; 3="Succeeded With Errors"; 4="Failed" ; 5="Aborted" }
Write-Warning "Failed installting $($update.Title) ResultCode $($installationResult.ResultCode) HResult $($installationResult.HResult)"
}
if($installationResult.HResult -eq 5) {
#$resultcode= @{0="Not Started"; 1="In Progress"; 2="Succeeded"; 3="Succeeded With Errors"; 4="Failed" ; 5="Aborted" }
Write-Warning "Aborted $($update.Title) ResultCode $($installationResult.ResultCode) HResult $($installationResult.HResult)"
}
}

if(!$needreboot) {
$arguments = @('/c wuauclt /reportnow')
Start-Process -FilePath "cmd.exe" -ArgumentList $arguments -Wait
}

if(!$skipReboot -and $needreboot){
write-progress -id 2 -ParentId 1 -Activity "Updating" -Status "Rebooting system"
Write-Warning "Reboot required"
Restart-Computer -Force
}
return $needreboot
}
End {
write-progress -id 1 -Activity "Updating" -Status "Windows Update Check Complete" -Completed
}