5 New Year’s Resolutions for Every SCCM Administrator

,

Maybe you’re like me and you’ve watched the first week of January come and go without giving a single thought to a New Year’s resolution. You already know you’re not going back to the gym and you’ve accepted that you can’t say no to pizza. So this year, instead of picking a resolution you know you won’t stick with, here are five New Year’s resolutions every Configuration Manager administrator could benefit from.

  1. Implement a front end into your task sequence

    Whether it’s TSLaunch, ConfigMgr OSD FrontEnd, or even MDT’s UDI, there is always value in implementing a front end into your OSD task sequence.My personal favorite is UI++, created by Jason Sandys. Its simple XML syntax, clear documentation, and frequent updates make it an easy choice if you’re just getting started.

    What should you do with it? Here are a few easy wins for any environment:

    • Implement a preflight to ensure only supported models are running your task sequence
    • Ask for the domain user name of the end user receiving the device so you can immediately associate that new hardware with a specific user in SCCM
    • Create a department dropdown that decides which applications are installed and which OU the machine is moved to

    If you already have a front end, that’s great! Implement something new that adds value to your service desk or end users. If you can’t think of any, I’m willing to bet your service desk can!

  2. Standardize your packaging process

    Keeping a standardized packaging process makes troubleshooting application installations easier for your frontline, your own team, and you! Implement something like the PowerShell Application Deployment Toolkit and make it a standard across all of the applications you and your team package this year.

    You don’t need to know PowerShell to start using PSAppDeployToolkit. The included Word document and simple cmdlet names make transitioning from batch files easy. Rich and automatic logging cranks out a verbose MSI installer log by default and paints a picture of what state the machine was in when the install began. Present prompts to end users and give them the option to defer application installations, all under a company branded banner.

    Whatever you choose to use to package with this year, put in the extra effort to package everything that way, no matter how simple the install.

  3. Eliminate Windows 7 and Windows Server 2008/R2 in your environment

    January 14th, 2020 is almost exactly a year away. If you already finished your Windows 10 migration a couple years ago, ensure the last handful of Windows 7 machines you have in your environment disappear. You know, that one sitting under the desk of the untouchable user no one wants to work with. It’s time to get management involved and kick that thing off your network.

    Also, don’t forget that Windows Server 2008 and 2008 R2 are also no longer supported on that same date. Work with your infrastructure team to ensure they have everything they need to build new VMs and enroll into your SCCM environment ahead of their big push to spin up new hardware. There’s nothing worse than discovering 100 newly built VMs aren’t in your SCCM environment or boundaries.

  4. Get certified

    In my opinion, the single most important thing you can do to stay relevant in IT is to keep getting certified. Certifications are the continuing education of the IT field. Make them mandatory for yourself to keep your skills sharp and relevant in the workplace.

    I’m personally studying for the new 70-703 to replace my older 70-243 and round out my MCSE. The 70-703 has been around long enough now that there is some great content on PluralSight you can get through in just about two weeks. Channel your inner Shia LaBeouf and just do it! Don’t let your dreams be dreams!

  5. Take on an apprentice

    See, you were rocking through this list until you hit this one. “I’ve already got a front end! We already use PSAppDeployToolkit! What even is Windows 7?!” Yeah, this is the tough one.Stop for a minute and think back to how you started working with ConfigMgr. Think about the very first time you learned what “/q” did to that installer you double-clicked a hundred times. And think about how appreciative you are towards those individuals that taught you those skills you use every single day.

    This doesn’t need to be anything formal. You don’t need a company sponsored internship or an interview process. Find that one guy or gal on your help desk that shows some passion in their work and take them out to lunch. Teach them how to silence an installer or, heck, how to read one of those PSAppDeployToolkit logs you just standardized all of your packaging around. Whatever it is, don’t forget that you’re in your current role because someone took you under their wing and made you a better tech.

Have a better SCCM-related New Year’s resolution in mind? Comment and share it below!

Cody Clements

Keeping the SCCM Cache Clean with DCM

, , , ,

In environments with frequent software distributions the SCCM cache folder can quickly take up large amounts of disk space.  This really becomes problematic on older systems or virtual machines with limited amounts of disk space.  Our support team found themselves having to constantly track down systems with low disk space and clean the cache.  I came up with this DCM configuration item to automatically detect and cleanup content in the cache which is older than the given number of days.  The detection and cleanup scripts both write to their own application event logs so you can see a history of cleanup activities.

Creating the CI

Create a new configuration item and select Windows Desktops and Servers as the type of configuration item.


Choose the appropriate operating systems.  In my case I selected all operating systems as I wanted the cache to be cleaned across the board.

Create a new setting and set it’s type to script and data type to string.

Add a discovery script for detecting old items in the cache folder.  Customize the number of days as you see fit.  You can also modify the event log source if desired.

$MinDays = 30

New-EventLog -LogName SCCM_Cleanup -Source "DCM" -ErrorAction SilentlyContinue
Write-EventLog -LogName SCCM_Cleanup -Source "DCM" -EntryType Information -EventId 1000 -Message "Detection starting for Cleanup CCMCACHE" -ErrorAction SilentlyContinue

$UIResourceMgr = New-Object -ComObject UIResource.UIResourceMgr
$Cache = $UIResourceMgr.GetCacheInfo()
$count = ($Cache.GetCacheElements() | where-object {[datetime]$_.LastReferenceTime -lt (get-date).adddays(-$mindays)} | Measure-object).Count

Write-EventLog -LogName SCCM_Cleanup -Source "DCM" -EntryType Information -EventId 1003 -Message "Total obsolete items found: $count" -ErrorAction SilentlyContinue
Write-EventLog -LogName SCCM_Cleanup -Source "DCM" -EntryType Information -EventId 1001 -Message "Detection ending for Cleanup CCMCACHE" -ErrorAction SilentlyContinue

$count

Next, define a remediation script.  Once again you can customize the number of days and event log name.  The number of days should match your detection script.

$MinDays = 30

New-EventLog -LogName SCCM_Cleanup -Source "DCM" -ErrorAction SilentlyContinue
Write-EventLog -LogName SCCM_Cleanup -Source "DCM" -EntryType Information -EventId 1010 -Message "Remediation starting for Cleanup CCMCACHE" -ErrorAction SilentlyContinue

$UIResourceMgr = New-Object -ComObject UIResource.UIResourceMgr
$Cache = $UIResourceMgr.GetCacheInfo()
$Cache.GetCacheElements() | where-object {[datetime]$_.LastReferenceTime -lt (get-date).adddays(-$mindays)} | foreach { $Cache.DeleteCacheElement($_.CacheElementID) }

Write-EventLog -LogName SCCM_Cleanup -Source "DCM" -EntryType Information -EventId 1011 -Message "Remediation ending for Cleanup CCMCACHE" -ErrorAction SilentlyContinue

The final step is to create your compliance rule.  Set the value to check against to 0 and check the run remediation script checkbox.


Now after you test the new CI assign it to the appropriate baseline(s) for your environment.  Now you can forget about having to manually cleanup the SCCM cache folder ever again.

Have you heard about Get-WQLObject?

, , , , ,

Have you heard about Get-WQLObject?


WQL is a part of SCCM administration whether you like it or not. Over time you may become quite savvy with writing up WQL queries and appreciate that it mimics SQL in just enough of a way to allow some fancy collections. 

You can also use the ‘Query’ section under monitoring to view, develop, and run these WQL queries outside of collection evaluation. This can be useful for getting some quick data in a one-off fashion at a point in time. Commonly I will need to interact with this data though such as when I’m remediating some issues via a script. As much fun as copy pasting to excel and importing a CSV is… I wrote a script instead! 


The Meat:


What I’m doing at it’s core is a WMI query. That is the real meat here. So… here’s a basic function that takes in a WQL query and gives you the raw output. 


$Query = @"
select distinct s.Name,
sw.ProductName,
sw.ProductVersion,
cbs.CNIsOnline,
os.Caption
from  SMS_R_System s
inner join SMS_G_System_INSTALLED_SOFTWARE sw on sw.ResourceID = s.ResourceId
inner join SMS_CollectionMemberClientBaselineStatus cbs ON cbs.ResourceID = s.ResourceId
inner join SMS_G_System_Operating_System os ON os.ResourceID = s.ResourceID
where sw.ProductName like "%7-Zip%"
and sw.ProductVersion NOT IN ("18.05.00.0","18.05")
order by sw.ProductVersion
"@

function Get-WQLObject {
    param(
        # WQL formatted query to perform
        [Parameter(Mandatory = $true)]
        [string]$Query,
        # SMS Provider to query against
        [Parameter(Mandatory = $true)]
        [string]$SMSProvider
    )
    $SiteCode = (Get-WmiObject -Namespace "root\sms" -ClassName "__Namespace" -ComputerName $SMSProvider).Name.Substring(5, 3)
    $Namespace = [string]::Format("root\sms\site_{0}", $SiteCode)
    Get-WmiObject -ComputerName $SMSProvider -Namespace $Namespace -Query $Query
}

Get-WQLObject -SMSProvider 'SCCM.CONTOSO.COM' -Query $Query

This almost shouldn’t even be a function, but it is because I put ‘function’ in front of it with a name and a {.

So, if you run this you will get some data back, though it is a bit ugly honestly. 

Ugly

But hey, this is PowerShell. It is object oriented. I can dig into this object. I can expand cbs (Founders Canadian Breakfast Stout anyone?) and then I can expand CNIsOnline and I’ll get the value I truly care about. Same for sw, and then ProductName, and ProductVersion. But that sounds tedious.


The Potatoes:


Right, so I technically have what I need. But I want more! Fatten things up a bit ya know? Couple scoops of potatoes. Disclaimer: This IS potatoes, it does slow things down a bit. Though to be fair, any large WQL query via PowerShell is going to be slow no matter what.


function Get-WQLObject {
    param(
        # WQL formatted query to perform
        [Parameter(Mandatory = $true)]
        [string]$Query,
        # SMS Provider to query against
        [Parameter(Mandatory = $true)]
        [string]$SMSProvider,
        # Optional PSCredential 
        [Parameter(Mandatory = $false)]
        [pscredential]$Credential
    )
    Begin {
        if ($PSBoundParameters.ContainsKey('Credential')) {
            $AddedDefaultParam = $true
            $PSDefaultParameterValues.Add("Get-WmiObject:Credential", $Credential)
        }
        $SiteCode = (Get-WmiObject -Namespace "root\sms" -ClassName "__Namespace" -ComputerName $SMSProvider).Name.Substring(5, 3)
        $Namespace = [string]::Format("root\sms\site_{0}", $SiteCode)
    }
    Process {
        $RawResults = Get-WmiObject -ComputerName $SMSProvider -Namespace $Namespace -Query $Query
        $PropertySelectors = $RawResults | Get-Member -MemberType Property | Where-Object { -not $_.Name.StartsWith('__') } | Select-Object -ExpandProperty name | ForEach-Object {
            $Class = $_
            $Properties = $RawResults.$Class | Get-Member -MemberType Property | Where-Object { -not $_.Name.StartsWith('__') } | Select-Object -ExpandProperty name
            foreach ($Property in $Properties) {
                [string]::Format("@{{Label='{1}.{0}';Expression = {{`$_.{1}.{0}}}}}", $Property, $Class)
            }
        }
    }
    end {
        if ($AddedDefaultParam) {
            $PSDefaultParameterValues.Remove("Get-WmiObject:Credential")
        }
        $PropertySelector = [scriptblock]::Create($($PropertySelectors -join ','))
        $RawResults | Select-Object -Property $(. $PropertySelector)
    }
}

Starting to look a lot more like a function! You know it should be a function when it starts doing weird things that you don’t feel like typing out over and over. 


$PropertySelectors = $RawResults | Get-Member -MemberType Property | Where-Object { -not $_.Name.StartsWith('__') } | Select-Object -ExpandProperty name | ForEach-Object {
    $Class = $_
    $Properties = $RawResults.$Class | Get-Member -MemberType Property | Where-Object { -not $_.Name.StartsWith('__') } | Select-Object -ExpandProperty name
    foreach ($Property in $Properties) {
        [string]::Format("@{{Label='{1}.{0}';Expression = {{`$_.{1}.{0}}}}}", $Property, $Class)
    }
}

Remember how I said we could ‘dig into’ the objects? This snippet above is generating a string (that we will join and turn into a scriptblock) to do just that. We are going to dynamically find our classes and properties by leveraging Get-Member and filtering out system properties. Those system properties don’t interest me in this context so I’m filtering them out with “Where-Object { -not $_.Name.StartsWith(‘__’) }” leaving me with the properties I care about.

Couple notes:

  •  I like [string]::Format. It is actually REALLY fast in terms of CPU time, and for me personally it makes things more ‘manageable.’ For some people it is just confusing. You can simply do $Potatoes = “$Var1″+’bacon’+”$Var2” or many other methods if you prefer. ($Var1 = ‘Cheese’ by the way)
  • In our generated string I am escaping $_ by writing out `$_. This is so that we don’t actually expand out $_ right now. I want to treat it is a string to be used later.

The output, based on the 7-Zip software query above, can be seen below and should look familiar if you’ve ever done some custom Select-Object with calculated properties.


@{Label='cbs.CNIsOnline';Expression = {$_.cbs.CNIsOnline}},
@{Label='os.Caption';Expression ={ $_.os.Caption}},
@{Label='s.Name';Expression = {$_.s.Name}},
@{Label='sw.ProductName';Expression ={$_.sw.ProductName}},
@{Label='sw.ProductVersion';Expression = {$_.sw.ProductVersion}}

To leverage this code in the way I am hoping to though, it cannot just be a string. It has to be a scriptblock that can be dot-sourced. Let’s do that!


 $PropertySelector = [scriptblock]::Create($($PropertySelectors -join ','))

Because we are going to be wanting to expand out from $_ in the context of $RawResults | Select-Object… we need to execute the scriptblock. Simply passing $PropertySelector won’t work, so instead we will dot-source the scriptblock.


$RawResults | Select-Object -Property $(. $PropertySelector)

This will ‘execute’ our $PropertySelector statement that we generated. So {$_.s.Name} will actually be {$<instance of $RawResults that we are piping>.s.Name} which is what we want!

And of course the output from the function now looks MUCH nicer.

Mmmm Potatoes!

The Gravy!


I know, I know. This has all been really dry. Enter… the gravy!

WQL… you’ve got that down right? You know a good 2-300 WMI Classes under the root\sms\site_<sitecode> namespace including their properties right? Yeah, me too. 

What if we could leverage all those queries you have under you ‘Queries’ node in monitoring?

$Gravy = ‘DynamicParam’


function Get-WQLObject {
    param(
        # WQL formatted query to perform
        [Parameter(Mandatory = $true, ParameterSetName = 'CustomQuery')]
        [string]
        $Query,
        # SMS Provider to query against
        [Parameter(Mandatory = $true)]
        [string]
        $SMSProvider,
        # Optional PSCredential (unfortunately I can't figure out how to use this cred in the DynamicParam WMI queries without providing info outside the function)
        [Parameter(Mandatory = $false, ParameterSetName = 'CustomQuery')]
        [pscredential]
        $Credential
    )
    DynamicParam {
        if (($SMSProvider = $PSBoundParameters['SMSProvider'])) {
            $ParameterName = 'SCCMQuery'
            $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
            $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
            $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
            $ParameterAttribute.Mandatory = $true
            $ParameterAttribute.ParameterSetName = 'ExistingQuery'
            $ParameterAttribute.HelpMessage = 'Specify the name of a query that already exists in your ConfigMgr environment'
            $AttributeCollection.Add($ParameterAttribute)
            $SiteCode = (Get-WmiObject -Namespace "root\sms" -ClassName "__Namespace" -ComputerName $SMSProvider).Name.Substring(5, 3)
            $Namespace = [string]::Format("root\sms\site_{0}", $SiteCode)
            $arrSet = Get-WmiObject -ComputerName $SMSProvider -Namespace $Namespace -Query "SELECT Name FROM SMS_Query WHERE Expression not like '%##PRM:%'" | Select-Object -ExpandProperty Name
            $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet)
            $AttributeCollection.Add($ValidateSetAttribute)
            $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
            $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
            return $RuntimeParameterDictionary
        }
    }
    Begin {
        $SCCMQuery = $PsBoundParameters[$ParameterName]
        if ($PSBoundParameters.ContainsKey('Credential') -and -not $PSDefaultParameterValues.ContainsKey("Get-WmiObject:Credential")) {
            $AddedDefaultParam = $true
            $PSDefaultParameterValues.Add("Get-WmiObject:Credential", $Credential)
        }
        $SiteCode = (Get-WmiObject -Namespace "root\sms" -ClassName "__Namespace" -ComputerName $SMSProvider).Name.Substring(5, 3)
        $Namespace = [string]::Format("root\sms\site_{0}", $SiteCode)
        if ($PSCmdlet.ParameterSetName -eq 'ExistingQuery') {
            $Query = Get-WmiObject -ComputerName $SMSProvider -Namespace $Namespace -Query "SELECT Expression FROM SMS_Query WHERE Name ='$SCCMQuery'" | Select-Object -ExpandProperty Expression
        }
    }
    Process {
        $RawResults = Get-WmiObject -ComputerName $SMSProvider -Namespace $Namespace -Query $Query
        $PropertySelectors = $RawResults | Get-Member -MemberType Property | Where-Object { -not $_.Name.StartsWith('__') } | Select-Object -ExpandProperty name | ForEach-Object {
            $Class = $_
            $Properties = $RawResults.$Class | Get-Member -MemberType Property | Where-Object { -not $_.Name.StartsWith('__') } | Select-Object -ExpandProperty name
            foreach ($Property in $Properties) {
                [string]::Format("@{{Label='{1}.{0}';Expression = {{`$_.{1}.{0}}}}}", $Property, $Class)
            }
        }
    }
    end {
        if ($AddedDefaultParam) {
            $PSDefaultParameterValues.Remove("Get-WmiObject:Credential")
        }
        $PropertySelector = [scriptblock]::Create($($PropertySelectors -join ','))
        $RawResults | Select-Object -Property $(. $PropertySelector)
    }
}

There we go! Now we are nice and bloated!

What have I done!!!

  • Added ParameterSets
  • Added DynamicParam
  • Justified creating a function by making this thing nice and ugly

We now have two ParameterSets, one of which is less obvious because it is introduced in the ‘DynamicParam’ block. 


$ParameterAttribute.ParameterSetName = 'ExistingQuery'

And then we do a bit of magic which allows us to tab-complete our existing Queries that you have in SCCM right now. (Note: I’m excluding those that require parameters because… yeah I don’t feel like writing in that logic right now)


$arrSet = Get-WmiObject -ComputerName $SMSProvider -Namespace $Namespace -Query "SELECT Name FROM SMS_Query WHERE Expression not like '%##PRM:%'" | Select-Object -ExpandProperty Name

So what does this do for me? 

Tab completion!

You now have tab completion based on the name of the queries in SCCM. You can execute all of your pre-existing queries, and even find them with tab-completion or by using ctrl+space.

Ohhh buddy!

I did mention this above, but I will say it again. A slow query is still a slow query, and our calculated properties isn’t going to make it faster. This function adds some overhead to your query as we are also piping it to a select-object after the fact. BUT it is pretty cool right? Get those gears turning on DynamicParams!

Some good-to-mentions about the function:

  • To use the -SCCMQuery parameter you need to supply -SMSProvider first. if (($SMSProvider = $PSBoundParameters[‘SMSProvider’]))
  • Unfortunately, no -Credential param when you are using -SCCMQuery because I wasn’t able to figure out how to use the credentials from the parameter inside the DynamicParam which would be needed to get our list of expressions. Let me know if you figure it out!
  • It is on GitHub
  • I might have made some gross oversights and overcomplicated this

Cody Mathis
@CodyMathis123