ConfigMgr and a Remote Content Library

ConfigMgr has since version 1806 been supporting a Remote Content Library, which in simplified terms means that you can decouple your existing infrastructure and place the Content Library on a file-share. There are a few caveats though…

Read the docs first to get the gist of things – it is always good to review the basics. After that – let’s get the details straight.

A great, even though some details changed, walk-through of the entire process is – including loads of great screenshots – Karthick’s blog

You most likely should verify that the share is located on a Windows-server with a NTFS-filesystem according to this thread.

Even thought it is documented a lot and pretty much everywhere, do note that the pointer to the content-library has to be a pre-existing folder in a file-share – not the path to the file-share itself.

If you expect to run this overnight – follow HappySCCMs advice and disable the backup temporarily. In addition – this seems to be the only place which actually describes a move experience based on some type of real workload. The initial copy performed by ConfigMgr doesn’t stop anything at the site, and can therefore be started during a non-maintenance window. A second copy status will actually halt any type of distribution (and wait for all ongoing distributions to be completed) – and that should occur during a planned time when there isn’t any business need to perform any updates to content.

Did you miss any of this and started a file-transfer anyways? Someone posted a fool-proof way to cancel an ongoing move in the forums – somewhat simplified here (and most likely not supported):

  • Fire up the Service Manager from Monitoring –> System Status –> Site Status

  • Query DistMgr service and stop it
  • Head onwards to the inboxes… Find the …\Microsoft Configuration Manager\inboxes\distmgr.box and delete any .CLM-file.
  • Next up is to start SQL Management Studio and find the CM-database. Do a query (in a query Window):
select * from ContentLibraryMove
  • Note the ID and then run the following query to clean it out
Delete from ContentLibraryMove Where ID = '____'

For some reason everyone keeps saying Service Window for this, but as far as I can tell – the packages are inserted and distributed as expected for the copy phase of it. After the initial copy this entry shows up;

WSUS Cleanup

Despite multiple articles of howto maintain a WSUS database for performance and scalability – there was always a performance issue dragging across which made all the cleanup jobs take forever. An earlier (not able to find it again) Technet Forum post clarified that this was due to all clean-up jobs beeing dependent on the Stored Procedure spDeleteUpdate, and this generates a temporary table that doesnt have an index for the appropiate columns. Unfortunately I werent able to find this post again – but lo, and behold – Microsoft confirmed the behaviour!

The fix is simply to alter the Stored Procedure spDeleteUpdate and append the following line during the creation of the temporary table;

image

Supersedence and how it is not a feature

Image result for configuration manager logoSupersedence was supposedly a great new feature for ConfigMgr 2012 (released in… you guessed it..) that was part of the Application Model. App-model held so many promises, however it was actually quite broken until a few service packs and fixes down the road. There are still quite a few defects that just are “known” by people and adds to the general feeling of ConfigMgr beeing complex.

After a twitter-rant on how Microsoft view their support organisation (and anyone paying / using it) as a third-class citizien with no rights – apart from beeing the therapist for whatever sysadmin is on the other end of the call – I became sort of frustrated that the sixth most top voted Uservoice item, something that I previously raised support calls within their organisation aswell as spent better part of a week (at Microsoft Seattle campus) ranting about this specific defect to just about anyone who potentially could reach the actual Product-Caretaker to fix the issue. Microsoft may spring out shiny new things – but it honestly sucks at fixing its products. If its broken – it’s still just plain broken. And even when its fixed – noone tells you its fixed apart from during watercooler talk.

How long has supersedence been broken? Since 2012

Update 2017-12-18:
ConfigMgr TechPreview 1712 was released with a specific fix that would potentially aim to resolve test 1 and 2. If you are on a support agreement it seems that potentially you could drop it and only relay requests via UserVoice.

What is the scenario?

So, a few things to get the setup of what the actual defect is. Supersedence is the intent to connect two Applications within ConfigMgr and inform the system how it should replace the older one/ones. For the defect to show itself the following has to be inplace;

  • Application has to be deployed (directly / indirectly) as Available
  • Application can be targeted to users / computers
  • Application made Available has to replace a previous old version that is installed on the given impacted client

FoxDeploy (Stephen Owen) wrote a great article that sets up the premise for the defect, however as far as I understand his target environment deals only with users (or user collection targeted deployments).  Apart from really sharing a few thoughts, creating a very popular UserVoice-item there are somethings that needs to be retested (previous extensive confirmation of this defect was in the ConfigMgr 2012 era – both for myself and FoxDeploy) with the newer versions of ConfigMgr.

There is no release statement from Microsoft that this has been altered, fixed or improved upon as far as the eye can see.

Based on the above we can also conclude that this is not relevant if;

  • All deployments are Required
  • Supersedence is not used
  • Older versions, now beeing replaced, are not installed on any endpoints

Tests

We will retry the following tests on a ConfigMgr 1702 site. Neither 1706 and 1710 has introduced anything remotely closing to fixing this behaviour and all rumours that people have heard about previous fixes have been pre-CurrentBranch.

Applications used while testing

image

Test 1

  • Application (Adobe Reader DC 18) will supersed an older version (Adobe Reader DC 17)  before creating the deployment
  • A deployment will be created that targets our specific computer that has Adobe Reader DC 17 installed
  • Deployment will target a computer collection
  • Deployment will be set to be made available

Result

When creating the deployment the following new check-boxes will become painfully obvious for the administrator. Pay attention, and ConfigMgr will tell you exactly how you are about to wreak havoc within the environment. Despite that you are saying – please just offer this as a nice-to-have-thingy – there is a greyed out check-box that indicates that a required deployment is technically beeing created.

image

When setting the date/time the deployment will be available – this becomes even more painfully obvious that you are infact creating a required deployment

image

As you might have guessed – as soon as the client receives this new policy – Adobe Acrobat Reader DC 17 is upgraded to Adobe Acrobat Reader DC 18.

Consistent with ConfigMgr 2012 in 2012

Test 2

  • Application (Adobe Reader DC 18) will supersed an older version (Adobe
    Reader DC 17)  after creating the deployment
  • A deployment will be created that targets our specific computer that
    has Adobe Reader DC 17 installed
  • Deployment will target a computer collection
  • Deployment will be set to be made available

Result

As you can imagine the wizard while creating the deployment as Available does not present any information regarding supersedence (as that relationship between the two applications does not exist yet)

image

After creating the supersedence relationship and then opening the properties for the specific Available deployment the following reveals itself. Same as when Foxdeploy concluded the testing early 2016.

image

The scheduling leaves no schedule set for when supersedence should be run.

image

For a brief moment on the client the two versions will reveal themselves

image

Unlike Foxdeploys testing (that were targeted for user collections) – the upgrade will still occur and now its running on the schedule that you can not control. At the latest of the next Application Evaluation cycle – the upgrade will take place.

Consistent with ConfigMgr 2012 in 2012

Test 3

  • Application (Adobe Reader DC 18) will supersed an older version (Adobe
    Reader DC 17) before deployment is created
  • The application themselves will not be deployed, but a Task Sequence containing one step installing Adobe Reader DC 18 will be made available.
  • A deployment (for the task sequence) will be created that targets our specific computer that
    has Adobe Reader DC 17 installed
  • Deployment will target a computer collection
  • Deployment will be set to be made available

Contents of Task Sequence

image

Result

As the task sequence only contains the step to install the one specific application there is no possibility to select that this would be available for PXE-only or other options than the ConfigMgr-client. The Task Sequence is still set to only be available. Per my experience it is also recommended to set the Available time back 1h (or 1 day) to get fairly instant results, otherwise you need to wait a bit before the client start processing the information.

image

As per the hallway chatter this seems to be resolved and the Adobe Acrobat Reader DC 18 does not cause an automatic upgrade.

Not consistent with ConfigMgr 2012 in 2012

Workaround

What have we learnt? Well – anything that depends on supersedence will have a required deployment instead of an available deployment. There are of course three options to deal with this and they are as follows

  • Script it. Powershell Application Deployment Toolkit offers great ways todo this with control.
  • Set up the supersedence relationship, and then create the available deployment and alter the time for the supersedence from ASAP to a date far far into the future. Perhaps so far that you will not be handling the site any more.
  • Follow FoxDeploys guidance for user deployments and create the deployment, and then the supersedence relationship.

 

 

 

ConfigMgr – Howto generate an inbox-backlog

Ever wondered how someone can generate an enormous inbox-backlog for a Configuration Manager site? Apart from not scaling the environment correctly based on the performance of the infrastructure and the number of clients and features its servicing – here are a two methods to bring pretty much everything to a halt.

Server Groups

Server Groups is a feature not yet released for production. It became available in ConfigMgr 1606 and has been worked on since. As one can check it is required to be enabled from Updates and Servicing under Administration in the Feature list, however its been toggled ‘On’ in a number of sites where noone has actively thought about using this feature. Perhaps its toggled on per default?

image

There are a great number of write-ups by independt specialists aswell as an extensive Microsoft documentation.

The Server Group is defined per collection, and there are a few safeguards in place to avoid harm – for example you can’t se this on the generic ConfigMgr collections – such as the All systems

image

Any other collection, regardless of how many members it has, can toggle this on.

image

Locating these collections that are enabled for Server Groups is fairly easy by simplying recursively searching for all collections that has Server Groups to Yes – check the Server Group search criteria.

image

image

Create the backlog

The intent is to control Software Updates and in what order they are applied. If Server Group is enabled for a collection with all (or many devices) and these devices have deadline for a Software Update set – things will happen directly after these deadlines. Each client will effectively send a status update regarding patching status roughly once every 5 minutes (or each minute?)

Now, watch that backlog go…

Relationships – Dependency or Supersedence

Deeply burried with a side-note on howto create Dependencies within a Configuration Manager application is the following limit specified;

In some cases, a deployment type is dependent on a deployment type that also contains dependencies. In this scenario, where a chain of dependencies exists, the maximum number of supported dependencies in the chain is five.
Source: Technet¨

After effective testing the following is concluded – this isn’t a hard-limit that measures the limit of 5 level deep dependency chain – but rather a complete halt of so many things when its to  ‘complex’ and ‘to large’. Or – its not the exact number of 5 that is the limit but rather that depending on loads of factors (complexity of application object, how many, how deep etc)  this will eventually generate a failure of the handling within the Configuration Client or simply to many status messages sent. Also – the limit is very applicable for supersedence.

A chain of 5 dependencies or supersedences is a very good estimate for when things will function, and beyond that you are at the mercy of luck and testing to see if things work.

What adds payload and therefore risk of failure?

  • Requirements
  • Detections
  • Conditions
  • Dependencies
  • Many levels of dependency
  • Supersedence
  • Many levels of supersedence

The more you have of the above the more has to be evaluated and processed. As a consultant I have learned the hardway to avoid using the ‘always’, ‘never’ etc. Usually time is saved if dependencies and supersedence is limited in use. Wrap it in whatever preferred scripting language you have.

Now, if you have neatly packaged and deployed all versions of Java or Adobe for the last 2-3 years – there should be quite a few versions available within ConfigMgr. Using supersedence this is a great way to start generating that backlog;

Create the backlog

Create the latest version and ensure you replace about 8 previous versions in the first level. After the first level there should be roughly 7 deeper levels for each one in the first level. Something in the lines of

image

Aim to deploy the application to any collection that contains many devices and watch that backlog start creeping up…

Why create this havoc?

The intent is not to teach novice users howto wreak havoc within an environment, but rather what potentially could cause a backlog in the ConfigMgr processing and howto identify ill advised practices within a company.

 

Software Metering–Checks and balances

Software Metering is a very interesting concept, however after been hit with quite a few issues of ensuring that we can depend on the reliable information within Configuration Manager – here comes an appendix to a great blog-post series provided by Microsoft

Software Metering Deep Dive and Automation Part 1: Use It Or Lose It – The Basics

Software Metering Deep Dive and Automation Part 2: Use It Or Lose It – The Collections

What is the issue?

For many reasons a few perfect storms essentially gave clients that from a far looked OK, but we could not consider their metering data accurate. Either it was blank (not the same as NULL), or it wasn’t good enough for only specific periods which caused them to inaccurately fall in-scope of not using a software at all. All of the reasons are of course part of the ongoing operations to ensure client health. However, users still had software removed and felt the frustration.

Checks and balances

We felt very confident in removing software, deploying accurately to devices which hadn’t use the software – apart from users being impacted by general issues of reporting metering data.

A check is to verify that there is metering data for the period that we want to ensure is software is used within. The following benefits are identified;

  • New devices wouldn’t be targeted until they have metering for the entire period
  • Devices which have black-out periods and fail to report any metering data for an extended period of time are excluded
  • Devices which are plain broken are excluded

Collections

Structure is to create a collection for each time period (month based) that you want to check. The specific example will use the following sample scenario;

  • A device will have the software removed if not used for 3 months (90 days)
  • We will verify that the device have metering data for period 0-30 days, 30-60 days and 60-90 days
  • The most efficient way to run the collection update is actually to have three separate collections
  • Each collection can be limited to the previous one
  • The collection with the deployment targeted can be limited to the final collection

Collection queries;

Metering check day 0 – 30

select SMS_R_SYSTEM.ResourceID,SMS_R_SYSTEM.ResourceType,SMS_R_SYSTEM.Name,SMS_R_SYSTEM.SMSUniqueIdentifier,SMS_R_SYSTEM.ResourceDomainORWorkgroup,SMS_R_SYSTEM.Client
from SMS_R_System  INNER JOIN SMS_MonthlyUsageSummary on SMS_R_SYSTEM.ResourceID = SMS_MonthlyUsageSummary.ResourceID
WHERE (DATEDIFF(day,SMS_MonthlyUsageSummary.LastUsage, GETDATE()) < 30
and DATEDIFF(day,SMS_MonthlyUsageSummary.LastUsage, GETDATE()) > 0)

Metering check day 30 – 60

select SMS_R_SYSTEM.ResourceID,SMS_R_SYSTEM.ResourceType,SMS_R_SYSTEM.Name,SMS_R_SYSTEM.SMSUniqueIdentifier,SMS_R_SYSTEM.ResourceDomainORWorkgroup,SMS_R_SYSTEM.Client
from SMS_R_System  INNER JOIN SMS_MonthlyUsageSummary on SMS_R_SYSTEM.ResourceID = SMS_MonthlyUsageSummary.ResourceID
WHERE (DATEDIFF(day,SMS_MonthlyUsageSummary.LastUsage, GETDATE()) < 60
and DATEDIFF(day,SMS_MonthlyUsageSummary.LastUsage, GETDATE()) > 30)

Metering check day 60 – 90

select SMS_R_SYSTEM.ResourceID,SMS_R_SYSTEM.ResourceType,SMS_R_SYSTEM.Name,SMS_R_SYSTEM.SMSUniqueIdentifier,SMS_R_SYSTEM.ResourceDomainORWorkgroup,SMS_R_SYSTEM.Client
from SMS_R_System  INNER JOIN SMS_MonthlyUsageSummary on SMS_R_SYSTEM.ResourceID = SMS_MonthlyUsageSummary.ResourceID
WHERE (DATEDIFF(day,SMS_MonthlyUsageSummary.LastUsage, GETDATE()) < 90
and DATEDIFF(day,SMS_MonthlyUsageSummary.LastUsage, GETDATE()) > 60)

AppsNotify 3.0

As a follow-up to a previous post – here comes a revised version of AppsNotify.

Changes

  • Possible to exclude applications that notify users
    Create a registry value under HKLM\Software\AppsNotify\AppsNotifyExclusion that matches the application that needs to be excluded
  • Detect if the C: – drive is low on disk and exit before attempt to write anything to disk
  • Notifications now include name of applications that are new, up to 5 new applications
  • Detect if the computer is idle and do not check AppCatalog and do not notify user
    -Avoids hammering of the website and notifications going nowhere
  • Log-file is generated in users temp folder: \appsnotify app.log

Code from elsewhere

Idle-time from StackOverflow
Most functions (not Logs – as previously stated in old blog-post) are from PowerShell Studio
Or written by me. See previous post.

Parameter

Pass on the parameter appcatalog with the url for Application Catalog – sample;

-appcatalog http://appcatalog.yourcompany.com:81/cmapplicationCatalog

Run it

Normally I wrap this within an executable that file (can run with Powershell 2/3 and should run as logged on user). How often? Well – sample code from an exported Task Scheduler

<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="<a href="http://schemas.microsoft.com/windows/2004/02/mit/task&quot;">http://schemas.microsoft.com/windows/2004/02/mit/task"</a>>
<RegistrationInfo>
<Author></Author>
</RegistrationInfo>
<Triggers>
<LogonTrigger>
<Repetition>
<Interval>PT10M</Interval>
<StopAtDurationEnd>false</StopAtDurationEnd>
</Repetition>
<StartBoundary>1899-12-30T06:04:14</StartBoundary>
<Enabled>true</Enabled>
<Delay>PT15M</Delay>
</LogonTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<GroupId>S-1-5-32-545</GroupId>
<RunLevel>LeastPrivilege</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>true</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>true</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>true</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>true</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT4H</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>"C:\Program Files (x86)\Common Files\AppsNotify\AppsNotify.exe"</Command>
<Arguments>-appcatalog http://website:8080/cmapplicationCatalog</Arguments>
</Exec>
</Actions>
</Task>

Code

Or download it

#========================================================================
# Created on:    2017-08-16
# Created by:    Nicke Källén
# Organization:
# Filename:        AppsNotify 3.0.pff
#========================================================================
Add-Type @'
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace PInvoke.Win32 {

public static class UserInput {

[DllImport("user32.dll", SetLastError=false)]
private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);

[StructLayout(LayoutKind.Sequential)]
private struct LASTINPUTINFO {
public uint cbSize;
public int dwTime;
}

public static DateTime LastInput {
get {
DateTime bootTime = DateTime.UtcNow.AddMilliseconds(-Environment.TickCount);
DateTime lastInput = bootTime.AddMilliseconds(LastInputTicks);
return lastInput;
}
}

public static TimeSpan IdleTime {
get {
return DateTime.UtcNow.Subtract(LastInput);
}
}

public static int LastInputTicks {
get {
LASTINPUTINFO lii = new LASTINPUTINFO();
lii.cbSize = (uint)Marshal.SizeOf(typeof(LASTINPUTINFO));
GetLastInputInfo(ref lii);
return lii.dwTime;
}
}
}
}
'@

$AppNotify.FormBorderStyle = 'FixedToolWindow'
Function Log-Start{
<#
.SYNOPSIS
Creates log file

.DESCRIPTION
Creates log file with path and name that is passed. Checks if log file exists, and if it does deletes it and creates a new one.
Once created, writes initial logging data

.PARAMETER LogPath
Mandatory. Path of where log is to be created. Example: C:\Windows\Temp

.PARAMETER LogName
Mandatory. Name of log file to be created. Example: Test_Script.log

.PARAMETER ScriptVersion
Mandatory. Version of the running script which will be written in the log. Example: 1.5

.INPUTS
Parameters above

.OUTPUTS
Log file created

.NOTES
Version:        1.0
Author:         Luca Sturlese
Creation Date:  10/05/12
Purpose/Change: Initial function development

Version:        1.1
Author:         Luca Sturlese
Creation Date:  19/05/12
Purpose/Change: Added debug mode support

.EXAMPLE
Log-Start -LogPath "C:\Windows\Temp" -LogName "Test_Script.log" -ScriptVersion "1.5"
#>

[CmdletBinding()]

Param ([Parameter(Mandatory=$true)][string]$LogPath, [Parameter(Mandatory=$true)][string]$LogName, [Parameter(Mandatory=$true)][string]$ScriptVersion)

Process{
$sFullPath = $LogPath + "\" + $LogName

#Check if file exists and delete if it does
If((Test-Path -Path $sFullPath)){
Remove-Item -Path $sFullPath -Force
}

#Create file and start logging
New-Item -Path $LogPath -Name $LogName –ItemType File

Add-Content -Path $sFullPath -Value "***************************************************************************************************"
Add-Content -Path $sFullPath -Value "Started processing at [$([DateTime]::Now)]."
Add-Content -Path $sFullPath -Value "***************************************************************************************************"
Add-Content -Path $sFullPath -Value ""
Add-Content -Path $sFullPath -Value "Running script version [$ScriptVersion]."
Add-Content -Path $sFullPath -Value ""
Add-Content -Path $sFullPath -Value "***************************************************************************************************"
Add-Content -Path $sFullPath -Value ""

#Write to screen for debug mode
Write-Debug "***************************************************************************************************"
Write-Debug "Started processing at [$([DateTime]::Now)]."
Write-Debug "***************************************************************************************************"
Write-Debug ""
Write-Debug "Running script version [$ScriptVersion]."
Write-Debug ""
Write-Debug "***************************************************************************************************"
Write-Debug ""

}
}

Function Log-Write{
<#
.SYNOPSIS
Writes to a log file

.DESCRIPTION
Appends a new line to the end of the specified log file

.PARAMETER LogPath
Mandatory. Full path of the log file you want to write to. Example: C:\Windows\Temp\Test_Script.log

.PARAMETER LineValue
Mandatory. The string that you want to write to the log

.INPUTS
Parameters above

.OUTPUTS
None

.NOTES
Version:        1.0
Author:         Luca Sturlese
Creation Date:  10/05/12
Purpose/Change: Initial function development

Version:        1.1
Author:         Luca Sturlese
Creation Date:  19/05/12
Purpose/Change: Added debug mode support

.EXAMPLE
Log-Write -LogPath "C:\Windows\Temp\Test_Script.log" -LineValue "This is a new line which I am appending to the end of the log file."
#>

[CmdletBinding()]

Param ([Parameter(Mandatory=$true)][string]$LogPath, [Parameter(Mandatory=$true)][string]$LineValue)

Process{
Add-Content -Path $LogPath -Value $LineValue

#Write to screen for debug mode
Write-Debug $LineValue
}
}

Function Log-Error{
<#
.SYNOPSIS
Writes an error to a log file

.DESCRIPTION
Writes the passed error to a new line at the end of the specified log file

.PARAMETER LogPath
Mandatory. Full path of the log file you want to write to. Example: C:\Windows\Temp\Test_Script.log

.PARAMETER ErrorDesc
Mandatory. The description of the error you want to pass (use $_.Exception)

.PARAMETER ExitGracefully
Mandatory. Boolean. If set to True, runs Log-Finish and then exits script

.INPUTS
Parameters above

.OUTPUTS
None

.NOTES
Version:        1.0
Author:         Luca Sturlese
Creation Date:  10/05/12
Purpose/Change: Initial function development

Version:        1.1
Author:         Luca Sturlese
Creation Date:  19/05/12
Purpose/Change: Added debug mode support. Added -ExitGracefully parameter functionality

.EXAMPLE
Log-Error -LogPath "C:\Windows\Temp\Test_Script.log" -ErrorDesc $_.Exception -ExitGracefully $True
#>

[CmdletBinding()]

Param ([Parameter(Mandatory=$true)][string]$LogPath, [Parameter(Mandatory=$true)][string]$ErrorDesc, [Parameter(Mandatory=$true)][boolean]$ExitGracefully)

Process{
Add-Content -Path $LogPath -Value "Error: An error has occurred [$ErrorDesc]."

#Write to screen for debug mode
Write-Debug "Error: An error has occurred [$ErrorDesc]."

#If $ExitGracefully = True then run Log-Finish and exit script
If ($ExitGracefully -eq $True){
Log-Finish -LogPath $LogPath
Break
}
}
}

Function Log-Finish{
<#
.SYNOPSIS
Write closing logging data & exit

.DESCRIPTION
Writes finishing logging data to specified log and then exits the calling script

.PARAMETER LogPath
Mandatory. Full path of the log file you want to write finishing data to. Example: C:\Windows\Temp\Test_Script.log

.PARAMETER NoExit
Optional. If this is set to True, then the function will not exit the calling script, so that further execution can occur

.INPUTS
Parameters above

.OUTPUTS
None

.NOTES
Version:        1.0
Author:         Luca Sturlese
Creation Date:  10/05/12
Purpose/Change: Initial function development

Version:        1.1
Author:         Luca Sturlese
Creation Date:  19/05/12
Purpose/Change: Added debug mode support

Version:        1.2
Author:         Luca Sturlese
Creation Date:  01/08/12
Purpose/Change: Added option to not exit calling script if required (via optional parameter)

.EXAMPLE
Log-Finish -LogPath "C:\Windows\Temp\Test_Script.log"

.EXAMPLE
Log-Finish -LogPath "C:\Windows\Temp\Test_Script.log" -NoExit $True
#>

[CmdletBinding()]

Param ([Parameter(Mandatory=$true)][string]$LogPath, [Parameter(Mandatory=$false)][string]$NoExit)

Process{
Add-Content -Path $LogPath -Value ""
Add-Content -Path $LogPath -Value "***************************************************************************************************"
Add-Content -Path $LogPath -Value "Finished processing at [$([DateTime]::Now)]."
Add-Content -Path $LogPath -Value "***************************************************************************************************"

#Write to screen for debug mode
Write-Debug ""
Write-Debug "***************************************************************************************************"
Write-Debug "Finished processing at [$([DateTime]::Now)]."
Write-Debug "***************************************************************************************************"

#Exit calling script if NoExit has not been specified or is set to False
If(!($NoExit) -or ($NoExit -eq $False)){
Exit
}

}
}
function Get-ScriptDirectory
{
if($hostinvocation -ne $null)
{
Split-Path $hostinvocation.MyCommand.path
}
else
{
Split-Path $script:MyInvocation.MyCommand.Path
}
}

function Parse-Commandline
{
<#
.SYNOPSIS
Parses the Commandline of a package executable

.DESCRIPTION
Parses the Commandline of a package executable

.PARAMETER  Commandline
The Commandline of the package executable

.EXAMPLE
$arguments = Parse-Commandline -Commandline $Commandline

.INPUTS
System.String

.OUTPUTS
System.Collections.Specialized.StringCollection
#>

[OutputType([System.Collections.Specialized.StringCollection])]
Param([string]$CommandLine)

$Arguments = New-Object System.Collections.Specialized.StringCollection

if($CommandLine)
{
#Find First Quote
$index = $CommandLine.IndexOf('"')

while ( $index -ne -1)
{#Continue as along as we find a quote
#Find Closing Quote
$closeIndex = $CommandLine.IndexOf('"',$index + 1)
if($closeIndex -eq -1)
{
break #Can’t find a match
}
$value = $CommandLine.Substring($index + 1,$closeIndex – ($index + 1))
[void]$Arguments.Add($value)
$index = $closeIndex

#Find First Quote
$index = $CommandLine.IndexOf('"',$index + 1)
}
}
return $Arguments
}

function Convert-CommandLineToDictionary
{
<#
.SYNOPSIS
Parses and converts the commandline of a packaged executable into a Dictionary

.DESCRIPTION
Parses and converts the commandline of a packaged executable into a Dictionary

.PARAMETER  Dictionary
The Dictionary to load the value pairs into.

.PARAMETER  CommandLine
The commandline of the package executable

.PARAMETER  ParamIndicator
The character used to indicate what is a parameter.

.EXAMPLE
$Dictionary = New-Object System.Collections.Specialized.StringDictionary
Convert-CommandLineToDictionary -Dictionary $Dictionary -CommandLine $Commandline  -ParamIndicator '-'
#>
Param(    [ValidateNotNull()]
[System.Collections.Specialized.StringDictionary]$Dictionary,
[string]$CommandLine,
[char] $ParamIndicator)

$Params = Parse-Commandline $CommandLine

for($index = 0; $index -lt $Params.Count; $index++)
{
[string]$param = $Params[$index]
#Clear the values
$key = ""
$value = ""

if($param.StartsWith($ParamIndicator))
{
#Remove the indicator
$key = $param.Remove(0,1)
if($index  + 1 -lt $Params.Count)
{
#Check if the next Argument is a parameter
[string]$param = $Params[$index + 1]
if($param.StartsWith($ParamIndicator) -ne $true )
{
#If it isn’t a parameter then set it as the value
$value = $param
$index++
}
}
$Dictionary[$key] = $value
}#else skip
}
}
function Get-ExclusionList
{
$REGExclusion = "HKLM:\Software\AppsNotify\AppsNotifyExclusion"
$excllist = @()
Log-Write -LogPath $logfilePath -LineValue "Start exclusion list creation"
If ($(Test-Path $REGExclusion))
{

##Discovery
$regKey = $REGExclusion

$p = Get-ItemProperty $REGExclusion

$p.PSObject.Properties |  foreach  { if (("PSPath","PSParentPath","PSChildName","PSDrive","PSProvider") -notcontains $_.name) { $excllist += $($_.name) } }

}
Log-Write -LogPath $logfilePath -LineValue $($excllist)
return $excllist
}

function Validate-IsURL
{
<#
.SYNOPSIS
Validates if input is an URL

.DESCRIPTION
Validates if input is an URL

.PARAMETER  Url
A string containing an URL address

.INPUTS
System.String

.OUTPUTS
System.Boolean
#>
[OutputType([Boolean])]
param ([string]$Url)

if($Url -eq $null)
{
return $false
}

return $Url -match "^(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-\.\?\,\'\/\\\+&amp;%\$#_]*)?$"
}

function Get-CMUserApps {
[CmdletBinding()]
param
(
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='URL for Application Catalogue')]
$url,
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='Path to logfile')]
$logfile,
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='Temp-file')]
$temp
)
Begin {
log-write -LogPath $logfile -LineValue "Create web service proxy"
$catalogurl = $url;
Log-Write -LogPath $logfile -LineValue "Connecting to $catalogurl"
try {
$url = $catalogurl+"/ApplicationViewService.asmx?WSDL";
$service = New-WebServiceProxy $url -UseDefaultCredential;

}
catch {
Log-Error -LogPath $logfile -ErrorDesc "AppCatalog no response" -ExitGraceFully $false
Log-Finish -LogPath $logfilePath -NoExit $true
break
}
}
Process {


$total = 0;
try {
Log-Write -LogPath $logfile -LineValue "Gathering applications"
$service.GetApplications("Name",$null,"Name","",100,0,$true,"PackageProgramName",$false,$null,[ref]$total) | select ApplicationId,Name | Export-Clixml $temp
return $true
}

catch {
Log-Error -LogPath $logfile -ErrorDesc $error[0] -ExitGraceFully $false
return $false
}
Remove-Variable -Name url
Remove-Variable -Name total
$service.dispose()

}

}

function Compare-CMUserApps {
[CmdletBinding()]
param
(
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='Permanent-file')]
$file,
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='Temp-file')]
$temp,
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='Path to logfile')]
$logfile
)
Process {
Log-Write -LogPath $logfile -LineValue "Comparing applications lists"
If (-Not (Test-Path $file)) {
Log-Write -LogPath $logfile -LineValue "No previous version of apps list"
try {
Rename-Item $temp "$prefix apps.xml"
}
catch {
Remove-Item $temp
Log-Error -LogPath $logfile -ErrorDesc "Unable to create initial list" -ExitGracefully $false
}

}
Else {

Log-Write -LogPath $logfile -LineValue "Starting check......"
# $diffs = (Compare-Object -ReferenceObject $(Get-Content $file) -DifferenceObject $(Get-Content $temp)) | Where {$_.SideIndicator -eq '<='}
# $diffsserver = (Compare-Object -ReferenceObject $(Get-Content $file) -DifferenceObject $(Get-Content $temp)) | Where {$_.SideIndicator -eq '=>'}

If ((Compare-Object -ReferenceObject $(Get-Content $file -ReadCount 0) -DifferenceObject $(Get-Content $temp -ReadCount 0)) -eq $null) {
Log-Write -LogPath $logfile -LineValue "No new applications"
Log-Write -LogPath $logfile -LineValue "Removing temporary file"

try {
Remove-Item $temp
}
catch {

Log-Error -LogPath $logfile -ErrorDesc "Unable to remove temp list" -ExitGracefully $false
}

}
Elseif (((Compare-Object -ReferenceObject $(Get-Content $file -ReadCount 0) -DifferenceObject $(Get-Content $temp -ReadCount 0)) | Where {$_.SideIndicator -eq '<='}) -ne $null -and ((Compare-Object -ReferenceObject $(Get-Content $file -ReadCount 0) -DifferenceObject $(Get-Content $temp -ReadCount 0)) | Where {$_.SideIndicator -eq '=>'}) -eq $null ) {
Log-Write -LogPath $logfile -LineValue "Less applications received"
try {
Log-Write -LogPath $logfile -LineValue "Remove permanent list"
Remove-Item $file
}
catch {
Remove-Item $temp
Log-Error -LogPath $logfile -ErrorDesc "Unable to remove permanent list" -ExitGracefully $false
}

try {
Log-Write -LogPath $logfile -LineValue "Rename temporary list"
Rename-Item $temp "$prefix apps.xml"
}
catch {
Log-Error -LogPath $logfile -ErrorDesc "Unable to switch temp-list to permanent" -ExitGracefully $false
}

}

Else {
Log-Write -LogPath $logfile -LineValue "New applications found"
#              $lastWrite = (get-item $file).LastWriteTime
#              $timespan = new-timespan -days 0 -hours 4 -minutes 5
#
#                if (((get-date) - $lastWrite) -gt $timespan) {
#                    Log-Write -LogPath $logfile -LineValue "File is older than 4 h, will reset"
#
#                         try {
#                            Log-Write -LogPath $logfile -LineValue "Remove permanent list"
#                            Remove-Item $file
#                          }
#                          catch {
#                                  Remove-Item $temp
#                                  Log-Error -LogPath $logfile -ErrorDesc "Unable to remove permanent list" -ExitGracefully $false
#                          }
#
#                          try {
#                                  Log-Write -LogPath $logfile -LineValue "Rename temporary list"
#                                  Rename-Item $temp "$prefix apps.xml"
#                                  }
#                          catch {
#                                   Log-Error -LogPath $logfile -ErrorDesc "Unable to switch temp-list to permanent" -ExitGracefully $false
#                                }
#                }
#                Else {
$newapps = $true
#                }

}

}
If ($newapps -eq $true) {
return $True

}
#Remove-Variable * -ErrorAction 'SilentlyContinue'
#$error.Clear()
#Clear-Host
#$diffs = $null
#$diffsserver = $null
}
}

function OnApplicationLoad {
#Note: This function is not called in Projects
#Note: This function runs before the form is created
#Note: To get the script directory in the Packager use: Split-Path $hostinvocation.MyCommand.path
#Note: To get the console output in the Packager (Windows Mode) use: $ConsoleOutput (Type: System.Collections.ArrayList)
#Important: Form controls cannot be accessed in this function
#TODO: Add snapins and custom code to validate the application load



return $true #return true for success or false for failure
}

function Get-NewAppCatalogApps {

[CmdletBinding()]
param
(
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='Permanent-file')]
$file,
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='Temp-file')]
$temp,
[Parameter(Mandatory=$True,
ValueFromPipelineByPropertyName=$True,
HelpMessage='Path to logfile')]
$logfile
)

Process {


try {
$array = @(Compare-Object $(Get-content $file -ReadCount 0) $(Get-Content $temp -ReadCount 0) | Where {$_.SideIndicator -eq '=>' -and $_.InputObject -match '<S N="Name">'} |Select-Object $_ -ExpandProperty InputObject)
}
catch {
Log-Error -LogPath $logfile -ErrorDesc "Unable to list new applications" -ExitGracefully $false
return
}

[array]$exclusionlist = Get-ExclusionList
Log-Write -LogPath $logfilePath -LineValue "Exclusions:"
Log-Write -LogPath $logfilePath -LineValue "$($exclusionlist)"
$intApps = $array.Length - 4
Log-Write -LogPath $logfilePath -LineValue "User has $(4+$intApps) new applications"
$i = 0
$applist = ""
foreach ($element in $Array) {

$element = $element.TrimStart(' ')
$element = $element -replace "</S>","`n"
$element = $element -replace "<S N=`"Name`">",""

if ($exclusionlist -contains $($element -replace "`n",""))
{

Log-Write -LogPath $logfilePath -LineValue "$($element -replace   `"`n`",`"`") is now excluded"
$array.remove($element)
$intApps-=1
}
else
{
$i++

if ($i -lt "5")
{

#$element = $element.TrimStart(' ')
#$element = $element.TrimEnd('</S>')
#$element = $element -replace "</S>","`n"
#$element = $element -replace "<S N=`"Name`">",""
$applist += $element

#$applist = $applist.TrimStart(' ')
}
else {
$applist = $applist + "and $intApps more`n"
return $applist
}
}
}
return $applist
}

}

function OnApplicationExit {
#Note: This function is not called in Projects
#Note: This function runs after the form is closed
#TODO: Add custom code to clean up and unload snapins when the application exits
#Log-Finish -LogPath $logfilePath -NoExit $true
$script:ExitCode = 0 #Set the exit code for the Packager
Log-Finish -LogPath $logfilePath -NoExit $false
break
}

$AppNotify_Load={
#TODO: Initialize Form Controls here
$NotifyIcon.Text = $list
#$NotifyIcon.BalloonTipText = $list
$NotifyIcon.ShowBalloonTip(30000,"New Applications Available",$list, 'Info')
}

#region Control Helper Functions
function Show-NotifyIcon
{
<#
.SYNOPSIS
Displays a NotifyIcon's balloon tip message in the taskbar's notification area.

.DESCRIPTION
Displays a NotifyIcon's a balloon tip message in the taskbar's notification area.

.PARAMETER NotifyIcon
The NotifyIcon control that will be displayed.

.PARAMETER BalloonTipText
Sets the text to display in the balloon tip.

.PARAMETER BalloonTipTitle
Sets the Title to display in the balloon tip.

.PARAMETER BalloonTipIcon
The icon to display in the ballon tip.

.PARAMETER Timeout
The time the ToolTip Balloon will remain visible in milliseconds.
Default: 0 - Uses windows default.
#>
param(
[Parameter(Mandatory = $true, Position = 0)]
[ValidateNotNull()]
[System.Windows.Forms.NotifyIcon]$NotifyIcon,
[Parameter(Mandatory = $true, Position = 1)]
[ValidateNotNullOrEmpty()]
[String]$BalloonTipText,
[Parameter(Position = 2)]
[String]$BalloonTipTitle = '',
[Parameter(Position = 3)]
[System.Windows.Forms.ToolTipIcon]$BalloonTipIcon = 'None',
[Parameter(Position = 4)]
[int]$Timeout = 0
)

if($NotifyIcon.Icon -eq $null)
{
#Set a Default Icon otherwise the balloon will not show
$NotifyIcon.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon([System.Windows.Forms.Application]::ExecutablePath)
}

$NotifyIcon.ShowBalloonTip($Timeout, $BalloonTipTitle, $BalloonTipText, $BalloonTipIcon)
}

#endregion

$NotifyIcon_MouseDoubleClick=[System.Windows.Forms.MouseEventHandler]{
#Event Argument: $_ = [System.Windows.Forms.MouseEventArgs]
#TODO: Place custom script here
Log-Write -LogPath $logfilePath -LineValue "User clicked icon"
Log-Write -LogPath $logfilePath -LineValue "Sending user to $appcatalog"
Start-Process $appcatalog

Log-Write -LogPath $logfilePath -LineValue "Timer started for $totaltime"
#Add TotalTime to current time
$script:StartTime = (Get-Date).AddSeconds($TotalTime)
#Start the timer
$timer1.Start()

}

$NotifyIcon_MouseClick=[System.Windows.Forms.MouseEventHandler]{
#Event Argument: $_ = [System.Windows.Forms.MouseEventArgs]
#$NotifyIcon.Visible = $true
$NotifyIcon.ShowBalloonTip(30000,"New Applications Available",$list, 'Info')
}

$NotifyIcon_BalloonTipClicked={
Log-Write -LogPath $logfilePath -LineValue "User clicked ballontip"
Log-Write -LogPath $logfilePath -LineValue "Sending user to $appcatalog"
Start-Process $appcatalog

Log-Write -LogPath $logfilePath -LineValue "Timer started for $totaltime"
#Add TotalTime to current time
$script:StartTime = (Get-Date).AddSeconds($TotalTime)
#Start the timer
$timer1.Start()

}

#Get path which scripts run from
$CurrentPath = Get-ScriptDirectory

#Import log-functions
#. "$CurrentPath\Logging_Functions.ps1"

&nbsp;

#Prefix for all generated files in user's %TEMP%
$prefix = "appsnotify"

#Logfile
$logfilePath = $env:temp+"\$prefix app.log"
#Visibility after icon-click
$TotalTime = 300
$TotalTime_All = 14300

if ((Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='C:'" | Select-Object -ExpandProperty FreeSpace) -lt "20000000" ) {
exit
}

$check=Get-Process AppsNotify -ErrorAction SilentlyContinue | Measure-Object

if ($check.count -lt "2") {

}
else {
Log-Error -LogPath $logfilePath -ErrorDesc "AppsNotify is already running. Terminating. " -ExitGraceFully $false
exit
}

#Temporary file to store applications
$tempfilePath  = $env:temp+"\$prefix app_temp.xml"
#Permanent file to store applications
$filePath = $env:temp +"\$prefix apps.xml"
#Reset log-file for this session
Remove-Item $logfilePath

&nbsp;

################################################################################################################
Log-Start -LogPath $env:temp -LogName "$prefix app.log" -ScriptVersion "2.0"

&nbsp;

#Verify that the $CommandLine variable exists
if($CommandLine -ne $null -and $CommandLine -ne "")
{
#Log-Write -LogPath $logfilePath -LineValue "There is a command-line"
Log-Write -LogPath $logfilePath -LineValue "Command-line is:"
Log-Write -LogPath $logfilePath -LineValue "$CommandLine"
#$Arguments = Parse-Commandline $CommandLine
#Convert the Arguments. Use – as the Argument Indicator
$Dictionary = New-Object System.Collections.Specialized.StringDictionary
Convert-CommandLineToDictionary -Dictionary $Dictionary -CommandLine $Commandline  -ParamIndicator '-'
}
else
{
#Not running in a packager or no command line arguments passed
Log-Error -LogPath $logfilePath -ErrorDesc "No command-line argument. Use -appcatalog <url>" -ExitGraceFully $false
Log-Finish -LogPath $logfilePath -NoExit $false
break
}

$appcatalog = $Dictionary["appcatalog"]
if($appcatalog -ne $null -and $appcatalog -ne "") {
Log-Write -LogPath $logfilePath -LineValue "Passed Application Catalog is $appcatalog"
if (Validate-IsURL -Url $appcatalog) {
Log-Write -LogPath $logfilePath -LineValue "Passed Application Catalog is a URL"
}
Else {
Log-Error -LogPath $logfilePath -ErrorDesc "This is not a url" -ExitGraceFully $false
Log-Finish -LogPath $logfilePath -NoExit $false
break
}

}
else {
#Address to Application Catalogue
Log-Error -LogPath $logfilePath -ErrorDesc "We need an Application Catalog" -ExitGraceFully $false
Log-Finish -LogPath $logfilePath -NoExit $false
break
}

if ($(([PInvoke.Win32.UserInput]::IdleTime).TotalMinutes) -gt 10) {
Log-Write -LogPath $logfilePath -LineValue "Idle Time: $(([PInvoke.Win32.UserInput]::IdleTime).TotalMinutes)"
Log-Write -LogPath $logfilePath -LineValue "No user at computer, exiting"
Log-Finish -LogPath $logfilePath -NoExit $false
break
}

if ((Get-CMUserApps -url $appcatalog -logfile $logfilePath -temp $tempfilePath) -eq $true) {

if ((Compare-CMUserApps -file $filePath -temp $tempfilePath -logfile $logfilePath) -eq $true) {
$list = "$(Get-NewAppCatalogApps -file $filePath -temp $tempfilePath -logfile $logfilePath)"
if ($list)
{
Log-Write -LogPath $logfilePath -LineValue "Applist is $list"
#$NotifyIcon.Text = $list
try {

$NotifyIcon.Visible = $true
#Add TotalTime to current time
Log-Write -LogPath $logfilePath -LineValue "Starting general timer..."
$script:StartTime_all = (Get-Date).AddSeconds($TotalTime_All)
#Start the timer
$timer_all.Start()

}
catch {
Log-Error -LogPath $logfilePath -ErrorDesc "Tray icon failed..." -ExitGraceFully $false
Log-Finish -LogPath $logfilePath -NoExit $false
break
}
finally {
try {
Log-Write -LogPath $logfilePath -LineValue "Removing $filepath"
Remove-Item $filePath
Log-Write -LogPath $logfilePath -LineValue "Renaming $tempfilePath"
Rename-Item -Path "$tempfilePath"  -NewName "$prefix apps.xml" -Force

}
catch {
Remove-Item $tempfilePath
Log-Error -LogPath $logfile -ErrorDesc "Unable to remove permanent list" -ExitGracefully $false
}
}
}
else {
try {
Log-Write -LogPath $logfilePath -LineValue "Removing $filepath"
Remove-Item $filePath
Log-Write -LogPath $logfilePath -LineValue "Renaming $tempfilePath"
Rename-Item -Path "$tempfilePath"  -NewName "$prefix apps.xml" -Force

}
catch {
Remove-Item $tempfilePath
Log-Error -LogPath $logfile -ErrorDesc "Unable to remove permanent list" -ExitGracefully $false
}
Rename-Item -Path "$tempfilePath"  -NewName "$prefix apps.xml" -Force
Log-Finish -LogPath $logfilePath -NoExit $false
break
}

}
Else {
Log-Finish -LogPath $logfilePath -NoExit $false
break
}

}
Else {
Log-Finish -LogPath $logfilePath -NoExit $false
break
}

&nbsp;

&nbsp;

$NotifyIcon_BalloonTipShown={
#TODO: Place custom script here
Log-Write -LogPath $logfilePath -LineValue "Notifying user"
}

$timer1_Tick={
#Use Get-Date for Time Accuracy
[TimeSpan]$span = $script:StartTime - (Get-Date)

#Update the display
#$formSampleTimer.Text = $labelTime.Text = "{0:N0}" -f $span.TotalSeconds

if($span.TotalSeconds -le 0)
{
Log-Write -LogPath $logfilePath -LineValue "Timer has passed"
$timer1.Stop()
$NotifyIcon.Visible = $false
$AppNotify.Close()
$NotifyIcon.Dispose()
Log-Finish -LogPath $logfilePath -NoExit $true
}
}

$timer_all_Tick={
#TODO: Place custom script here
#Use Get-Date for Time Accuracy
[TimeSpan]$span = $script:StartTime_all - (Get-Date)

#Update the display
#$formSampleTimer.Text = $labelTime.Text = "{0:N0}" -f $span.TotalSeconds

if($span.TotalSeconds -le 0)
{
Log-Write -LogPath $logfilePath -LineValue "General timer is up.. closing..."
$timer_all.Stop()
$NotifyIcon.Visible = $false
$AppNotify.Close()
$NotifyIcon.Dispose()
Log-Finish -LogPath $logfilePath -NoExit $true
}
}

CM, IP-ranges and unknown networks

A follow-up of a previous post relating to matching ConfigMgr IP-range boundaries to known networks. The essence is to send an email (scheduled at your own interval) that notifies if there are any clients on unknown networks inventoried.

Prerequisites

  • Configuration Manager is required to have IP-ranges for boundaries
  • We assume that boundaries are /24, or 255.255.255.0 and IPv4
  • Clients need to collect and report Network Data
  • The user running the script needs to be able to connect and read data from Configuration Manager database
  • SMTP-server to send an email

What do we do?

  • Gather all active IP-range boundaries from database
  • Gather reported networks that matches the DNS-Suffix defined, IPv4 and sum up # of devices within /24-networks

Loads of assumptions….  incase you need to tweak it this is how we gather from the database.

Client networks – alter for $netquery. Remember to replace $dnsdomain

select 
SUBSTRING(ip.IPAddress0, 1, LEN(ip.IPAddress0) - CHARINDEX('.',REVERSE(ip.IPAddress0))) + '.1' As IP,
COUNT(*) as Devices
 from v_Network_DATA_Serialized as ip 
where ip.IPAddress0 IS NOT NULL and 
ip.IPSubnet0 != '64'  and 
ip.IPSubnet0 != '128' and 
ip.IPSubnet0 = '255.255.255.0' and 
ip.DNSDomain0 IS NOT NULL and 
ip.DNSDomain0 = '$dnsdomain' and 
ip.TimeStamp > DATEADD(day, -10, GETDATE()) 
GROUP BY  SUBSTRING(ip.IPAddress0, 1, LEN(ip.IPAddress0) - CHARINDEX('.',REVERSE(ip.IPAddress0))) 
ORDER BY Devices DESC

Boundaries – alter for $query

select 
bound.DisplayName, 
SUBSTRING(bound.value,1,CHARINDEX('-',bound.value) -1) AS LEFTHALF,
SUBSTRING(bound.value,CHARINDEX('-',bound.value) +1 ,100) AS RIGHTHALF 
from vSMS_Boundary as bound 
where 
bound.BoundaryType = '3' and 
bound.GroupCount > 0

Output – email

An email is sent with the following information after running the script

Network – network (always ends with a .1)
Devices – number of devices
DNSDomain – DNS-suffix
IPSubnet – network mask
DefaultGateway – default gateway
DHCPServer – DHCP-server

Parameters

Before running the actual script the following is required to be updated. DNSDomain – what DNS-suffix your clients are reporting as.

#Database params
$ErrorActionPreference = "silentlycontinue"
#Database-server
$datasource = "DBSERVER"
#Database
$database = "CM_DATABASE"
#DNS-Domain
$dnsdomain = 'dns.suffix.se'

#Email params
$EmailParams = @{
To         = 'email'
From       = 'email'
Smtpserver = 'smtp.company.se'
Subject    = "ConfigMgr Client Unknown Networks -  $(Get-Date -Format dd-MMM-yyyy)"
}

Script

#========================================================================
# Created with: Powershell ISE
# Created on:   2017-08-13
# Created by:   NiKa
# Organization:
# Filename:     CM_BoundaryCheck.ps1
#========================================================================

function IsIpAddressInRange {
param(
[string] $ipAddress,
[string] $fromAddress,
[string] $toAddress
)

$ip = [system.net.ipaddress]::Parse($ipAddress).GetAddressBytes()
[array]::Reverse($ip)
$ip = [system.BitConverter]::ToUInt32($ip, 0)

$from = [system.net.ipaddress]::Parse($fromAddress).GetAddressBytes()
[array]::Reverse($from)
$from = [system.BitConverter]::ToUInt32($from, 0)

$to = [system.net.ipaddress]::Parse($toAddress).GetAddressBytes()
[array]::Reverse($to)
$to = [system.BitConverter]::ToUInt32($to, 0)

$from -le $ip -and $ip -le $to
}

###### Parameters #################

#Database params
$ErrorActionPreference = "silentlycontinue"
#Database-server
$datasource = "DBSERVER"
#Database
$database = "CM_DATABASE"
#DNS-Domain
$dnsdomain = 'dns.suffix.se'

#Email params
$EmailParams = @{
To         = 'email'
From       = 'email'
Smtpserver = 'smtp.company.se'
Subject    = "ConfigMgr Client Unknown Networks -  $(Get-Date -Format dd-MMM-yyyy)"
}
###### Parameters #################

#### Retrieve client networks
$netquery = "select SUBSTRING(ip.IPAddress0, 1, LEN(ip.IPAddress0) - CHARINDEX('.',REVERSE(ip.IPAddress0))) + '.1' As IP, COUNT(*) as Devices from v_Network_DATA_Serialized as ip where ip.IPAddress0 IS NOT NULL and ip.IPSubnet0 != '64'  and ip.IPSubnet0 != '128' and ip.IPSubnet0 = '255.255.255.0' and ip.DNSDomain0 IS NOT NULL and ip.DNSDomain0 = '$dnsdomain' and ip.TimeStamp > DATEADD(day, -10, GETDATE()) GROUP BY  SUBSTRING(ip.IPAddress0, 1, LEN(ip.IPAddress0) - CHARINDEX('.',REVERSE(ip.IPAddress0))) ORDER BY Devices DESC"

$networks= Invoke-Sqlcmd -Query $netquery -server $datasource -Database $database

#### Retrieve boundaries
$query = "select bound.DisplayName, SUBSTRING(bound.value,1,CHARINDEX('-',bound.value) -1) AS LEFTHALF,SUBSTRING(bound.value,CHARINDEX('-',bound.value) +1 ,100) AS RIGHTHALF from vSMS_Boundary as bound where bound.BoundaryType = '3' and bound.GroupCount > 0"

$iprange = Invoke-Sqlcmd -Query $query -server $datasource -Database $database

#### Check if IP-address are within boundaries
$report=@()
foreach ($net in $networks) {
if (!($net.ip -eq '192.168.1.1' -or $net.ip -eq '0.0.0.1'  -or $net.ip -eq '10.10.0.1' -or $net.ip -eq '172.16.0.1' -or $net.ip -eq '169.254.43.1' -or $net.ip -eq '169.254.36.1' -or $net.ip -eq '192.168.0.1' -or $net.ip -eq '10.0.100.1')) {
$i = 0
$J = $iprange.count
$boundaryfound = $false
do {
#$iprange[$i].displayname
if (IsIpAddressInRange $net.ip $iprange[$i].LEFTHALF $iprange[$i].RIGHTHALF)
{

$boundaryfound = $true
}
$i++
} until ($i -gt $j)
if ($boundaryfound -eq $false)
{
#write-host "Network: $($net.ip) - Devices: $($net.Devices)"

#Retrieve information about network
$devquery = "select distinct DNSDomain0,IPSubnet0,DefaultIPGateway0,DHCPServer0 from v_Network_DATA_Serialized as ip
where ip.IPAddress0 IS NOT NULL
and ip.IPSubnet0 != '64'
and ip.TimeStamp > DATEADD(day, -10, GETDATE())
and ip.IPaddress0 like '$($($net.ip) -replace ".$")%'"
$devices= Invoke-Sqlcmd -Query $devquery -server $datasource -Database $database
$report += New-Object psobject -Property @{Network=$($net.ip);Devices=$($net.devices);DNSDomain=$($devices.DNSDomain0);IPSubnet=$($devices.IPSubnet0);DefaultGateway=$($devices.DefaultIPGateway0);DHCPServer=$($devices.DHCPServer0)}
}
}
}

if ($report -ne $null) {

#$report

#Generate email

$style = @"
<style>
body {
color:#333333;
font-family: ""Trebuchet MS"", Arial, Helvetica, sans-serif;}
}
h1 {
text-align:center;
}
h2 {
border-top:1px solid #666666;
}
table {
border-collapse: collapse;
font-family: ""Trebuchet MS"", Arial, Helvetica, sans-serif;
}
th {
font-size: 10pt;
text-align: left;
padding-top: 5px;
padding-bottom: 4px;
background-color: #1FE093;
color: #ffffff;
}
td {
font-size: 8pt;
border: 1px solid #1FE093;
padding: 3px 7px 2px 7px;
}
</style>

"@

$Properties = @(
'Network',
'Devices',
'DNSDomain',
'IPSubnet',
'DefaultGateway',
'DHCPServer'
)

$body = $report |
Select-Object -Property $Properties|
ConvertTo-Html -Head $style -Body "
<H3>Devices from unknown networks ($($results.Count))</H3>

" |
Out-String

Send-MailMessage @EmailParams -Body $Body -BodyAsHtml

}

Inventory % for App-V and ARP

A SQL query that needs to run against the Configuration Manager database and present all software that is installed for a specific collection (App-V or traditionally registered software in Programs and Features)

Will present;

Software Name
Software Version
App-V or ProductCode
# installations
% av devices that have the software installed (in comparision to collection members)
# Collection Members

 

DECLARE @collection VARCHAR(50),
@DisplayName VARCHAR(50);

SET @collection = 'SS100517';
set @DisplayName = 'Adobe Audition%'

select arp.DisplayName0 as 'Name', arp.Version0 as 'Version', arp.ProdID0 as 'GlobalorProd', COUNT(*) as Total, count(distinct arp.resourceid) * 100/
(
SELECT count(distinct ws.resourceid)
from v_ClientCollectionMembers ws
where ws.CollectionID = @collection
) as '%',
(
SELECT count(distinct ws.resourceid)
from v_ClientCollectionMembers ws
where ws.CollectionID = @collection
) as 'Collection Members'
from v_Add_Remove_Programs as arp
inner join v_ClientCollectionMembers as coll on arp.ResourceID = coll.ResourceID
where arp.DisplayName0 LIKE @DisplayName
and  CollectionID = @collection
GROUP BY arp.DisplayName0, arp.Version0, arp.ProdID0
UNION
select appv.Name00 as 'Name', appv.Version00 as 'Version', 'AppV' as 'GlobalorProd', COUNT(*) as Total,
count(distinct appv.machineid) * 100/
(
SELECT count(distinct ws.resourceid)
from v_ClientCollectionMembers ws
where ws.CollectionID = @collection
) as '%',
(
SELECT count(distinct ws.resourceid)
from v_ClientCollectionMembers ws
where ws.CollectionID = @collection
) as 'Collection Members'

from APPV_CLIENT_PACKAGE_DATA as appv
inner join v_ClientCollectionMembers as coll on appv.MachineID = coll.ResourceID
where appv.Name00 LIKE @DisplayName
and  CollectionID = @collection
GROUP BY appv.Name00, appv.Version00, appv.PackageId00

Device, primary user and model

A simple query that will present the following information based on what ConfigMgr has inventoried

Computer-name
Primary User(Domain\username)
Manufacturer
Model

SQL;

select umr.MachineResourceName as 'Device name', umr.UniqueUserName as 'Username',
CASE
when CAST(umsr.SourceID as varchar(20))  = '1' then 'Software Catalog'
when CAST(umsr.SourceID  as varchar(20)) =  '2' then 'Administrator'
when CAST(umsr.SourceID as varchar(20)) =  '3'  then 'User'
when CAST(umsr.SourceID as varchar(20)) =  '4'  then 'Usage agent'
when CAST(umsr.SourceID as varchar(20)) =  '5'  then 'Device Management'
when CAST(umsr.SourceID as varchar(20)) =  '6'  then 'OSD'
when CAST(umsr.SourceID as varchar(20)) =  '7'  then 'Fast Install'
else CAST(umsr.SourceID as varchar(20)) END as 'Source',
cs.Manufacturer0 AS 'Manufacturer',
CASE
when cs.Manufacturer0 = 'Lenovo' Then hwdata.Version0
else cs.Model0
END as Model
from v_UserMachineRelationship as umr
inner join v_UserMachineSourceRelation as umsr on umr.RelationshipResourceID = umsr.RelationshipResourceID
inner JOIN v_GS_COMPUTER_SYSTEM AS cs ON umr.MachineResourceID = cs.ResourceID
left join v_GS_COMPUTER_SYSTEM_PRODUCT as hwdata on umr.MachineResourceID = hwdata.resourceid

Windows 7 x64 and TPM 2.0

If you are using Bitlocker, Configuration Manager 1610 or higher and get a piece of hardware that has TPM 2.0 there are a few more hurdles to get the device encrypted. Coretech has written notes from the field which states the two options going forward. Following their recommendation – lets go down the path of running a device with CSM (UEFI) enabled and installing Windows 7.

Make sure that the device is running in UEFI and is setup as expected. I will leave the actual configuration of BIOS / UEFI out of this – as any device with TPM 2.0 will most likely have a preconfigured settings that are optimal. If not, time to configure for UEFI (without Secure Boot).

UEFI Boot

Once that is completed – create a new group that has the intention to identify if we (if possible) booted with a legacy option (legacy PXE-boot, legacy USB stick etc etc). Validate that its not UEFI-booted and that we have a TPM 2.0 chip (and add a check that we are also intending to install Windows 7 x64).

image

Only two steps are required in the group – format the drive as GPT and then a restart to the boot-image. Suggested format:

image

Preprovision

Once we have ensured that the device is running in UEFI, and not legacy…, and since we all are running a newer ADK then 1511 – we should add three registry keys per Microsoft

Above configuration of the algorithms for Windows 10 (build 1511) is kept as REG_DWORDs under:

HKLM\SOFTWARE\Policies\Microsoft\FVE

Operating System drives: EncryptionMethodWithXtsOs

Fixed Data drives: EncryptionMethodWithXtsFdv

Removable Data drives: EncryptionMethodWithXtsRdv

Hotfix

You can integrate the TPM 2.0 hotfix into the Windows 7, or simply install it after the WIM has been copied onto the machine. It should be installed before MBAM (or just bitlocker) is installed and starts handling the encryption