Get Powershell ISE to run scripts with -Verbose flag

Missing an easy way to run your scripts from ISE with the -Verbose or -Debug flag?

Easy to add… Open up your Microsoft.PowerShellISE_profile.ps1 file and add the following lines:

$psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add('Run with -Verbose', { Invoke-Expression -Command ". '$($psISE.CurrentFile.FullPath)' -Verbose" }, 'Ctrl+F5') | Out-Null
$psISE.CurrentPowerShellTab.AddOnsMenu.Submenus.Add('Run with -Debug',   { Invoke-Expression -Command ". '$($psISE.CurrentFile.FullPath)' -Debug" }, 'Ctrl+F6') | Out-Null

Now when you restart ISE you should see the options under the Add-Ons menu.

Create Site Roles Collections

Quick and easy way to create device collections based on site roles:

$wmiParams = @{
    "Namespace" = "root\SMS\site_ABC";
    "Query" = "SELECT RoleName FROM SMS_SystemResourceList"
}

Get-wmiobject @wmiParams  | Group-Object RoleName | ForEach-Object {
    Write-Verbose "Creating collection for role $($_.Name)"
    $newCollection = New-CMDeviceCollection -Name "ConfigMgr-Role $($_.Name)" -LimitingCollectionID "SMS00001" -RefreshType Periodic -RefreshSchedule (New-CMSchedule -RecurInterval Days -RecurCount 4 -Start (Get-Date))
    $query = "SELECT * FROM SMS_R_System WHERE SMS_R_System.SystemRoles = '$($_.Name)'"
    Add-CMDeviceCollectionQueryMembershipRule -CollectionId $newCollection.CollectionID -QueryExpression $query -RuleName "CMRole-$($_.Name)"
}

You will end up with collections like:
- ConfigMgr-Role SMS Distribution Point
- ConfigMgr-Role SMS Device Management Point
- ConfigMgr-Role … and so on …

Stand Alone Media and USB 3

I’m testing some Stand Alone USB Media for deployment of Windows 7 and 8…

Ran in to a few problems…

First off, it’s not to easy to find a USB-Stick with at least 32 Gb that is bootable.
We found that Kingston DataTraveler R3.0 (rubber like casing) works OK to boot from…

Second, keep the volume size under 32 GB.
Since it will be a FAT32 Volume you need to create a partition at 32 (or less) GB. (Remember that it needs to be active)

Third, when using a USB3 port on the computer you will probably run in to problems if you try to install USB3-drivers.
Workaround is to plug the stick in to a USB2 port.

Fourth, when only having USB3 ports you can run in to issues accessing the files.
A simple workaround is to get a simple USB-Hub that uses USB2… with that you will downgrade your USB3-stick to a USB2-stick and everything works just fine. :)

Fifht, when using custom scripts in MDT you might run in to issues if you use “Start in” and then point out something like “%deployroot%\MyScripts” as folder and a command line like “cscript script.vbs”
To solve this, use a command line like “cscript %DeployRoot%\MyScripts\script.vbs” and empty out the “Start in” folder.

Get filename in ConfigMgr 2012 ContentLib

In ConfigMgr 2007 it was kind of convinient to be able to edit files directly on a DP, in ConfigMgr 2012 that isn’t to easy if you are using Content Library.

There are a few ways to find out where the files are actually stored, here is one way:

(Will give you the path to somefile.xml in package ABC01234)

PARAM (
	$siteCode = "ABC",
	$FileName = "somefile.xml",
	$PackageID = "ABC01234",
	$ContentLib = "D:\SCCMContentLib"
)
Import-Module ($Env:SMS_ADMIN_UI_PATH.Substring(0,$Env:SMS_ADMIN_UI_PATH.Length-5) + '\ConfigurationManager.psd1')
Set-Location "$($siteCode):"


$package = Get-CMPackage -ID $PackageID
$hash = (Get-Content "$($ContentLib)\DataLib\$($package.PackageID).$($package.StoredPkgVersion)\$($FileName).INI" | Where-Object { $_ -like "Hash=*" }).Replace("Hash=", "")
$storePath =  Join-Path "$($ContentLib)\FileLib" "$($hash.Substring(0,4))\$($hash)"

$sourcePath = Join-Path $package.PkgSourcePath $FileName

Write-Host "Source: $($sourcePath)"
Write-Host "Store : $($storePath)"

Will result in something like this:

Source: \\server\share\path\to\package source\somefile.xml
Store : D:\SCCMContentLib\FileLib\A123\A1234567890ABCDEF1234567890ABCDEF

And with this you can edit the XML-file directly on the store instead of editing source, update DPs, wait for processing, wait some more…

… but, I’m not saying that I recommend anyone to edit files directly in the store. ;)

ConfigMgr Package Status Reports

Found a nice idea for a report on Eswar Koneti’s blog. That query combined with some info from a blogpost by Jörgen Nilsson will give you two reports to dig into status of packages.

Status of Distribution Points with Package Compliance

SELECT DISTINCT 
	CDR.DPNALPath AS DPNalPath,
	UPPER(SUBSTRING(CDR.DPNALPath,13,CHARINDEX('.', CDR.DPNALPath) -13)) AS ServerName,
	CDR.PkgCount AS Targeted,
	CDR.NumberInstalled AS Installed,
	CDR.PkgCount-CDR.NumberInstalled AS NotInstalled,
	PSd.SiteCode AS ReportingSite,
	ROUND((100 * CDR.NumberInstalled/CDR.pkgcount), 2) AS Compliance

FROM
	v_ContentDistributionReport_DP CDR LEFT JOIN v_PackageStatusDistPointsSumm PSd
	ON CDR.DPNALPath=PSD.ServerNALPath

This report can be linked into the next one (using DPNalPath as a parameter)

Package Compliance on a single Distribution Point

Updated 2013-10-08 Join in tables instead of Select Case on State and PackageType

SELECT
	v_ContentDistribution.State AS StateNo,
	DPStatusInfo.StateName AS State,
	v_ContentDistribution.PkgID AS PackageID,
	v_ContentDistribution.PackageType AS PackageTypeNo,
	SMSPackageTypes.Name AS PackageType,
	SUBSTRING(v_ContentDistribution.Path,CHARINDEX(']', v_ContentDistribution.Path)+1, LEN(v_ContentDistribution.Path) - CHARINDEX(']', v_ContentDistribution.Path)-1) AS PackagePath,
	v_ContentDistribution.SiteCode,
	v_ContentDistribution.SourceVersion,
	v_ContentDistribution.SummaryDate

FROM
	v_ContentDistribution LEFT JOIN DPStatusInfo ON v_ContentDistribution.State = DPStatusInfo.State
	LEFT JOIN SMSPackageTypes ON v_ContentDistribution.PackageType = SMSPackageTypes.PackageTypeID

WHERE DistributionPoint = @DistributionPoint

This report need a parameter for DistributionPoint, to list all in a drop-down, use the query below on the parameter. (If you want to use the report without going thru a link)

SELECT DISTINCT
	ServerNALPath,
	SiteCode + ' - ' + SUBSTRING(ServerNALPath,CHARINDEX(']\\', ServerNALPath)+3, LEN(ServerNALPath) - CHARINDEX(']\\', ServerNALPath)-3) AS DistributionPoint
FROM v_DistributionPoint

Update ConfigMgr packages with Hotfix-information

When you install a hotfix and/or a cumulative update in ConfigMgr you can select the option to let the installer create some packages.

But, those packages are missing some info… For instance Manufacturer and Version.

A quick and dirty SQL update will do the trick.

UPDATE SMSPackages_G
SET
	Version = LEFT(Replace(Source, '\\server.domain.com\SMS_ABC\hotfix\', ''), 9),
	Manufacturer = 'Microsoft',
	Language = 'All'
WHERE Source LIKE '\\server.domain.com\SMS_ABC\Hotfix%'

This is of course not supported… but afaik it works. :D

If you want to see what would be updated, run this first

SELECT
	PkgID,
	Version AS OrgVersion,
	LEFT(Replace(Source, '\\server.domain.com\SMS_ABC\hotfix\', ''), 9) AS NewVersion,
	Manufacturer AS OrgManufacturer,
	'Microsoft' AS NewManufacturer,
	Language AS OrgLanguage,
	'All' AS NewLanguage
FROM SMSPackages_G
WHERE Source LIKE '\\server.domain.com\SMS_ABC\hotfix\%'

(And yes, you need to replace servername and sitecode)

Get-ADSites and create Site Based Collections

Missing an easy way to get AD-Sites from Powershell?

They are listed under Sistes/Configuration with the objectClass = Site. So a simple LDAP-query does the trick.

Get-ADSites {
	Return Get-AdObject -LdapFilter "(ObjectClass=site)" -SearchBase "CN=Sites,CN=Configuration,$((Get-AdDomain).DistinguishedName)"
}

Then, with these sites it’s easy to create Device Collections in ConfigMgr based on what AD-Site the client reports.

Get-ADSites | ForEach-Object {
	$newCollection = New-CMDeviceCollection -Name "AD-Site $($_.Name)" -LimitingCollectionID "SMS00001" -RefreshType Periodic -RefreshSchedule (New-CMSchedule -RecurInterval Days -RecurCount 1 -Start (Get-Date))
	$query = "SELECT * FROM SMS_R_System WHERE SMS_R_System.ADSiteName = '$($_.Name)'"
	Add-CMDeviceCollectionQueryMembershipRule -CollectionId $newCollection.CollectionID -QueryExpression $query -RuleName "ADSite-$($_.Name)"
}

Reset local admin password

You probably want to have random passwords on all your local admin accounts… Wrote a function that generates a complex (and readable) password and another function that sets the local admin password.

Function Get-RandomPassword {
	PARAM (
		$pwdMask = "####-####-####-####-####",
		$pwdCharacters = "abcdefghjkmnopqrstuvwxy23456789ABCDEFGHJKLMNPQRTUVWXYZ"
	)

	$newPassword = ""
	(0 .. (($pwdMask.Length)-1) ) | ForEach-Object  {
		If ( $pwdMask.Chars($_) -eq "#" ) {
			$rndChar = Get-Random -Minimum 0 -Maximum $pwdCharacters.Length
	 		$newPassword += $pwdCharacters.Chars($rndChar)
		} else {
			$newPassword += $pwdMask.Chars($_)
		}
	}

	Return $newPassword
}


Function Set-LocalAdminPassword {
	PARAM (
		[string] $computerName,
		[string] $newPassword
	)

	$adminAccountName = (Get-WmiObject Win32_UserAccount -Filter "LocalAccount = True AND SID LIKE 'S-1-5-21-%-500'" -ComputerName $computerName | Select-Object -First 1 ).Name
	TRY {
		Write-Verbose "Reset password for $($computerName)\$($adminAccountName) to $($newPassword)"
		$adminAccount = [adsi]"WinNT://$($computerName)/$($adminAccountName),user"
		$adminAccount.setPassword($newPassword)
		Return $true
	}
	CATCH {
		Return $false
	}
}

This is how to reset password on a single computer

Set-LocalAdminPassword -computerName "SOMEPC" -newPassword (Get-RandomPassword)

And if you want to process a list of computers (that are online) from the AD

Import-Module ActiveDirectory
$newPwds = @{}
Get-ADComputer -LDAPFilter "(name=PC00*)" | ForEach-Object {
	$computerName = $_.Name
	If (Test-Connection -ComputerName $computerName -Count 1 -ErrorAction SilentlyContinue) {
		$randomPwd = Get-RandomPassword
		If (Set-LocalAdminPassword -computerName $computerName -newPassword $randomPwd ) {
			$newPwds.Add($computerName, $randomPwd)
		}
	}
}

$newPwds | Format-Table -AutoSize

Remove all direct memberships from collections

If you are using direct memberships to speed up ConfigMgr, you probably want to clean it up when your collection query is good to go…

The problem is to find all direct memberships since Get-CMUserCollectionDirectMembershipRule doesn’t allow wildcards. But there is a WMI-class with all members…

Function Remove-AllDirectMemberships {
	PARAM (
		[string] $sccmSite = "ABC",
		[string] $collectionNameFilter = "SomePrefix*"
	)

	Get-CMUserCollection -Name $collectionNameFilter | ForEach-Object {
		$Collection = $_
		Write-host "$($Collection.Name)"
		Get-WmiObject -Class $_.MemberClassName -Namespace "ROOT\SMS\Site_$($sccmSite)" | Foreach-Object {
			If ($_.IsDirect -eq $true ) {
				Write-host "- Removing $($_.ResourceID)"
				Remove-CMUserCollectionDirectMembershipRule -CollectionId $Collection.CollectionID -ResourceId $_.ResourceID -Force
			}
		}
	}
}

Default Powershell Execution Policy

You can use a GPO to set the ExecutionPolicy to a static value on all machines.

But what if you want to default it to something and then let the users have the ability to change it?

Group Policy Preferences is the easy answer.

Create a GPO targeting your machines and then create a entry under Computer Configuration -> Preferences -> Windows Settings -> Registry that looks like this:
130917-execpolicy-1

130917-execpolicy-2

The paths are:
Key: SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell
Value Name: ExecutionPolicy
ValueData: RemoteSigned

With this setup a machine that doesn’t have any specific exec policy (then the reg key doesn’t exist) setup will get RemoteSigned.
(Another way is to do this with a startup script for the computer)

Calendar in Powershell CLI

Working on a nice way of creating informational background images… Stay tuned, it might be more posts on that later on… :)

Anyway, started with a small snippet that creates a calendar in CLI

PARAM (
	[DateTime] $StartDate = (Get-Date)
)

$startDay = Get-Date (Get-Date $StartDate -Format "yyyy-MM-01")

Write-Host (Get-Date $startDate -Format "MMMM yyyy")
Write-Host "Mo Tu We Th Fr Sa Su"
For ($i=1; $i -lt (get-date $startDay).dayOfWeek.value__; $i++) {
	Write-Host "   " -noNewLine
}

$processDate = $startDay
while ($processDate -lt $startDay.AddMonths(1)) {
	Write-Host (Get-Date $processDate -Format "dd ") -NoNewLine
    
    if ((get-date $processDate).dayOfWeek.value__ -eq 0) { Write-Host "" }
	$processDate = $processDate.AddDays(1)
}
Write-Host ""

Will return something like this:

C:\snowland> .\CalendarCreator.ps1 -StartDate "2013-02-01"
february 2013
Mo Tu We Th Fr Sa Su
            01 02 03
04 05 06 07 08 09 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28
C:\snowland>

Bitlocker Info

I’m playing around with pipelining information to functions… and since I needed a function to read Bitlocker information from Active Directory, why not create one. :)

Function Get-BitlockerInfo {
	PARAM (
		[Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true)] $Computer
	)

	BEGIN {
		$bitLockerInfo = @()
	}
	
	PROCESS {
		Write-Verbose "Searching $($Computer.DistinguishedName) ..."
		Get-ADObject -LdapFilter "(msFVE-Recoverypassword=*)" -Searchbase $Computer.DistinguishedName -properties msFVE-RecoveryPassword | ForEach-Object {
			$Bitlocker = $_.Name.Split("{")
			$retObj = New-Object -TypeName System.Object
			$retObj | add-Member -memberType NoteProperty -name ComputerDistinguishedName -Value $Computer.DistinguishedName
			$retObj | add-Member -memberType NoteProperty -name BitlockerTime -Value $Bitlocker[0]
			$retObj | add-Member -memberType NoteProperty -name PasswordID -Value $Bitlocker[1].Replace("}", "")
			$retObj | add-Member -memberType NoteProperty -name RecoveryPassword -Value $_."msFVE-RecoveryPassword"

			$bitLockerInfo += $retObj
		}
	}
	
	END {
		Return $bitLockerInfo
	}
}

# Here is how to use it
Get-AdComputer -LdapFilter "(name=WKS012*)" | Get-BitlockerInfo | Format-List

Change settings on all shares

We are enabling BranchCache on a few servers… and with that you probably want to change the settings on the shares you have.

Why not use some Powershell when you have the chance. :)

Get-WmiObject -Class Win32_Share | Where-Object -FilterScript {$_.Name -notlike '?$' -and $_.Name -ne "ADMIN$" -and $_.Path -ne ""} | ForEach-Object {
	Write-Host "Updating $($_.Name) - $($_.Path)"
	$netShare = NET SHARE $_.Name /Cache:BranchCache
	
	If ($netShare -notlike "*completed successfully*") {
		Write-Host $netShare -ForegroundColor Red
	}
}

Change /Cache:BranchCache to wahtever you want to do with the share.

Oh, btw… this works fine to do remote with something like this:

Invoke-Command -ComputerName SOMEREMOTESERVER -scriptblock {
	Get-WmiObject -Class  ... and the same code here ...
}

Your Online/Offline state for a offline files enabled share

Want to know the online/offline state of a DFS-share that is in your offline files?

There is a WMI class called Win32_OfflineFilesConnectionInfo that you can use. Of course it’s not as easy as just querying that one…

You can do it one one compressed line…

((Get-WmiObject Win32_OfflineFilesItem -Filter "ItemType = 2 AND ItemPath = '\\\\SNOWLAND.SE\\MyRoot'").ConnectionInfo).ConnectState

Or a bit more elegant with a function…

Function Get-DfsRootConnectionState {
	PARAM (
		[string] $rootName,
		[switch] $asText
	)
	$ConnectStates = @("Unknown", "Offline", "Online")
	$OfflineReasons = @("Unknown", "Not applicable", "Working offline", "Slow connection", "Net disconnected", "Need to sync item", "Item suspended")

	$rootName = $rootName.Replace("\", "\\")
	$root = Get-WmiObject Win32_OfflineFilesItem -Filter "ItemType = 2 AND ItemPath = '$($rootName)'"

	$stateAsText = "$($ConnectStates[$root.ConnectionInfo.ConnectState]) - $($OfflineReasons[$root.ConnectionInfo.OfflineReason])"
	Write-Verbose $stateAsText
	
	If ($asText) {
		Return $stateAsText
	} else {
		Return $root.ConnectionInfo.ConnectState
	}
}

Get-DfsRootConnectionState -rootName "\\SNOWLAND.SE\MyRoot" -asText

User pictures in AD

Adding pictures to Active Directory is kind of nice, it gives you pictures in Outlook, Lync and few other products…

To add it you need to load a picture as binary and put it in to the attribute thumbnailPhoto on the user.

Function Add-AdThumbnailPhoto {
    PARAM (
        [ValidateScript({Test-Path $_ -PathType Leaf})] [string] $PicturePath,
        $UserAccount
    )
    
    If (!(Test-IsModuleLoaded "ActiveDirectory")) {
        Throw "You need to run: Import-Module ActiveDirectory"
    }
    Write-Verbose "Adding $($PicturePath) to $($UserAccount)"
    $pictureBinary = [byte[]](Get-Content $PicturePath -Encoding byte)
    
    If ([System.Text.Encoding]::ASCII.GetString($pictureBinary).Length -ge 100Kb) {
        Throw "Picture to large, max size is 100Kb"
    }
    
    
    Try {
        Set-AdUser $UserAccount -Replace @{ thumbnailPhoto = $pictureBinary }
    }
    Catch {
        Throw $error[0]
    }
}

Then you can use the function like this:

Add-AdThumbnailPhoto -PicturePath "C:\MyPicture.jpg" -UserAccount "MyUserAccount"

If you like to do it on a oneliner… here is a bit more compressed version of the same code:

Set-AdUser "MyUserAccount" -Replace @{ thumbnailPhoto = ([byte[]](Get-Content "C:\MyPicture.jpg" -Encoding byte) }