Posts Tagged ‘SCCM 2012’

For those of you who have ever attended one of Johan Arwidmark’s talks or classes on OS deployment, you were probably convinced, like me, to capture your reference image using MDT LiteTouch for deployment with ConfigMgr. There are several advantages to capturing your reference image with LiteTouch (speed, compatibility, delegation, features) and there can also be a few disadvantages. One of those is if you have a thick or fat image, i.e. you have a lot of applications installed in your reference image.

Most of the applications you have in your MDT environment need to be maintained in your Configuration Manager environment as well, so that you can deploy the latest versions to your SCCM clients. So, when the time comes to update your reference image, you need to create all of those applications again in MDT.

Enter the “Convert-CMApptoMDTApp” PowerShell script. This script converts applications created in ConfigMgr 2012 SP1 to MDT applications. It utilises the new Configuration Manager 2012 SP1 and MDT 2012 PowerShell modules.

To create an application in MDT, we need a few bits of information:

Name, ShortName, Version, SoftwareVersion, Publisher, Language, CommmandLine, WorkingDirectory, ApplicationSourcePath, DestinationFolder

These can be retrieved from the the ConfigMgr application using the Get-CMDeploymentType cmdlet. We get the latest revisions by specifying the IsLatest property = True.

The Get-CMDeploymentType cmdlet is a strange fish and I had to play around with it quite a bit to understand the information it returns.

When you run the Get-CMDeploymentType cmdlet using the -ApplicationName parameter, you get back an array of deployment types. Each deployment type has a property called SDMPackageXML, which contains most of the interesting information that we need. Firstly, we need to convert this object from XML using the [Microsoft.ConfigurationManagement.ApplicationManagement.Serialization.SccmSerializer]::DeserializeFromString() method so that we can easily access its properties.

Now, each SDMPackageXML object has a property called DeploymentTypes. If you have an application with one Deployment type, the DeploymentTypes property will return information about that one deployment type. However, things start to get a little confusing when you have multiple deployment types for an application.

Let me explain.

1. You have an Application called App1, with a deployment type DT1, which has a version of V1. You run the Get-CMDeploymentType cmdlet. It returns one deployment type. The DeploymentTypes property of that deployment types returns information about DT1 v1.

2. You add a second deployment type to App1, called DT2 v1. You run the Get-CMDeploymentType cmdlet. It returns two deployment types. The DeploymentTypes property of DT1 returns information about DT1. The DeploymentTypes property of DT2 returns information about DT1 v1 and DT2 v1.

3. You make a change to deployment type DT1 v1 – it is now v2. You run the Get-CMDeploymentType cmdlet. It returns two deployment types. The DeploymentTypes property of DT1 returns information about DT1 v2 and DT2 v1. The DeploymentTypes property of DT2 returns information about DT1 v1 and DT2 v1.

So, to summarise the behaviour of the cmdlet when there are multiple deployment types: The most recently modified deployment type contains the most up-to-date information about all deployment types for that application, while less recently modified deployment types contain information about themselves and the versions of other deployment types that existed when they were modified. Weird, huh?

So, to make sure we look at the most recent version of a deployment type in the DeploymentTypes property for that specific deployment type, we need to compare the ID and version of the deployment type using the CI_UniqueID property and the DeploymentTypes.ID property:


Compare-Object -ReferenceObject $dtUniqueID -DifferenceObject $dtInstance.ID -IncludeEqual -Property Name,Version | Where { $_.SideIndicator -eq "==" }

Another issue I encountered is that MDT won’t create applications with the same name or destination folder, but multiple deployment types can share the same application name and different applications might have deployments types with the same name. So we need to append a number to each duplicate application name to ensure they are unique.

Here’s a nice one-liner that takes care of this problem:


$cmAppDT | Group-Object -Property Name -ErrorAction SilentlyContinue | Where { Foreach-object { $_.count -gt 1; $i=1} } -ErrorAction SilentlyContinue  | Select Group -ExpandProperty Group -ErrorAction SilentlyContinue | Foreach-object { Add-Member -InputObject $_ -membertype NoteProperty -name Name -value ($_.Name + "_" + $i) -Force -ErrorAction SilentlyContinue; Add-Member -InputObject $_ -membertype NoteProperty -name DestinationFolder -value ($_.DestinationFolder + "_" + $i) -Force -ErrorAction SilentlyContinue; $i++  }

If you have two deployment types that share the same application name “Application”, these will be renamed Application_1 and Application_2.

Now that we have those small details out of the way, let’s see the script in action.

The script will get a list of all of your CM12 applications and display the list using the Out-GridView cmdlet. This lets you select the applications you want to convert.

CMApps

Once you have selected the applications for conversion, the script runs the Get-CMDeploymentType cmdlet against each one and displays a progress indicator. This can take some time, depending on how many applications you have selected.

DT1

It will only look at MSI and Script deployment types, extract all the necessary information to create an application in MDT and then display the list of deployment types, again using the Out-GridView cmdlet. This lets you select the deployment types you want to convert.

MDTApps

Once you have selected the deployment types for conversion, they are passed to the Import-MDTApplication cmdlet to be created in MDT.

MDTImport

Script Pre-requisites:

Appropriate permissions in ConfigMgr and MDT.
The Configuration Manager 2012 SP1 PowerShell Module is expected in this directory:
“C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1”
The MDT 2012 PowerShell Module is expected in this directory:
“C:\Program Files\Microsoft Deployment Toolkit\bin\MicrosoftDeploymentToolkit.psd1”

Running the script:

To run the script, save it as “Convert-CMApptoMDTApp.ps1”. Launch a PowerShell (x86) console as administrator. You can set the values of the parameters in the script, or you can run the script with parameters, e.g.

Convert-CMApptoMDTApp.ps1 -CMSiteCode “CM1” -CMSiteServer “CMServer1” -MDTSharePath “\\MDTServer1\MDTShare” -MDTAppFolderName “CMApps”

The script:


Param (
    [ValidateNotNullOrEmpty()]
    [string]$CMSiteCode = "CM1",
    [ValidateNotNullOrEmpty()]
    [string]$CMSiteServer = "CMServer",
    [ValidateNotNullOrEmpty()]
    [string]$MDTSharePath = "\\MDTServer\MDTShare",
    [ValidateNotNullOrEmpty()]
    [string]$MDTAppFolderName = "CMApps"
)

# Import CM12 and MDT 2012 PowerShell modules
Import-Module "C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1"
Import-Module "C:\Program Files\Microsoft Deployment Toolkit\bin\MicrosoftDeploymentToolkit.psd1"

# Set the working directory to the CM Site code
CD ($cmSiteCode + ":")

# Get a list of all CM applications
$cmApps = Get-WmiObject -ComputerName $cmSiteServer -Namespace "Root\SMS\Site_$cmSiteCode" -Query "Select LocalizedDisplayName from SMS_Application Where IsLatest='True'" | Sort LocalizedDisplayName
# Output the list of applications to a Grid to allow browsing and selecting of the CM Apps to be converted to MDT Apps.
$cmApps = $cmApps | Out-GridView -PassThru

# Array to hold all of the CM deployment types
$cmAppDT = @()
# Counter for the progress of CM App processing.
$cmAppIndicator = 0
# Counter for the progress of conversion to MDT Apps.
$mdtAppIndicator = 0

If ($cmApps -ne $null) {
    Foreach ($cmApp in $cmApps) {
        $cmAppsCount = @($cmApps).Count
        $cmAppIndicator++
        Write-Progress -Activity "Processing Application $cmAppIndicator of $cmAppsCount" -Status $cmApp.LocalizedDisplayName -PercentComplete ($cmAppIndicator / $cmAppsCount * 100)

        # Get a list of the deployment types for each application
        $cmDeploymentType = Get-CMDeploymentType -ApplicationName ($cmApp | Select LocalizedDisplayName -ExpandProperty LocalizedDisplayName)

        # Enumerate the latest deployment types and get the latest SDMPackageVersion
        Foreach ($dt in $cmDeploymentType | Where { $_.IsLatest -eq $true }) {
            $SDMPackageXML = $dt | Select SDMPackageXML -ExpandProperty SDMPackageXML
            If ($dt.Technology -match "Script" -or $dt.Technology -match "MSI") {
                If ($SDMPackageXML -ne "") {
                    $dtInfo = [Microsoft.ConfigurationManagement.ApplicationManagement.Serialization.SccmSerializer]::DeserializeFromString($SDMPackageXML)
                    $dtCI_UniqueID = $dt.CI_UniqueID -split "/"
                    $dtUniqueID = @()
                    # Get the Deployment Type ID and version
                    $dtUniqueID = New-Object PSObject -Property @{
                        Name     =    $dtCI_UniqueID[1]
                        Version =    $dtCI_UniqueID[2]
                    }
                    $dtInstances = $dtInfo.DeploymentTypes
                    Foreach ($dtInstance in $dtInstances) {
                        # Compare the DT ID and Version to those contained in the DeploymentTypes property to make sure we get the most recent version of the deployment type and only this deployment type in our ForEach loop.
                        If ((Compare-Object -ReferenceObject $dtUniqueID -DifferenceObject $dtInstance.ID -IncludeEqual -Property Name,Version | Where { $_.SideIndicator -eq "==" } ) -ne $null ) {
                            $dtInstaller = $dtInstance | Select Installer -ExpandProperty Installer
                            If ($dtInstaller.Technology -match "Script" -or $dtInstaller.Technology -match "MSI") {
                                # If the working directory of the CM App is a local drive or environment variable, set the MDT App working directory accordingly
                                If ($dtInstaller.InstallFolder -match ":" -or $dtInstaller.InstallFolder -match "%") {
                                    $dtWorkingDirectory = $dtInstaller.InstallFolder
                                }
                                # Otherwise, set the MDT working directory to the root of the MDT application we are creating.
                                Else {
                                    $dtWorkingDirectory = ".\Applications\" + $dtInfo.Title
                                }
                                # Create a custom PS object with the information from the CM App DT we need to create the MDT App
                                $cmAppDT += New-Object PSObject -Property @{
                                    Name                    =     $dtInfo.Title
                                    ShortName                =     $dtInstance.Title
                                    Version                    =     $dtInfo.SoftwareVersion
                                    Publisher                 =     $dtInfo.Publisher
                                    Language                 =    ($dtInstance | Select Languages -ExpandProperty Languages) -join ","
                                    CommandLine             =    $dtInstaller.InstallCommandLine
                                    WorkingDirectory         =    $dtWorkingDirectory
                                    ApplicationSourcePath    =    ($dtInstaller | Select Contents -ExpandProperty Contents | Select Location -ExpandProperty Location)
                                    DestinationFolder         =    $dtInfo.Title
                                }
                            }
                        }
                    }
                }
                Else {
                    $dtName = $dt.LocalizedDisplayName
                    Write-Host "$dtName has no SDMPackage information"
                }
            }
        }
    }

    If ($cmAppDT -ne $null) {

        # Multiple deployment types can share the same application name and different applications might have deployments types with the same name.
        # MDT won't allow applications with the same name or destination folder, so we need to append a number to each duplicate deployment type to ensure they are unique.
        $cmAppDT | Group-Object -Property Name -ErrorAction SilentlyContinue | Where { Foreach-object { $_.count -gt 1; $i=1} } -ErrorAction SilentlyContinue  | Select Group -ExpandProperty Group -ErrorAction SilentlyContinue | Foreach-object { Add-Member -InputObject $_ -membertype NoteProperty -name Name -value ($_.Name + "_" + $i) -Force -ErrorAction SilentlyContinue; Add-Member -InputObject $_ -membertype NoteProperty -name DestinationFolder -value ($_.DestinationFolder + "_" + $i) -Force -ErrorAction SilentlyContinue; $i++  }

        # Output the Deployment Types to a Grid to allow browsing and selecting of the CM Apps to be converted to MDT Apps.
        $cmAppDTsToConvertToMDTApps = $cmAppDT | Sort -Property Name | Out-GridView -PassThru

        If ($cmAppDTsToConvertToMDTApps -ne $null) {
            $mdtAppCount = @($cmAppDTsToConvertToMDTApps).Count
            # Create a new PSDrive pointing to the MDT share
            New-PSDrive -Name "DS001" -PSProvider MDTProvider -Root $mdtSharePath | Out-Null
            # Create a folder under the MDT Applications folder for our imported CM applications.
            If (!(Test-Path (Join-Path "DS001:\Applications" $MDTAppFolderName))) {
                New-Item -path "DS001:\Applications" -Enable "True" -Name $MDTAppFolderName -Comments "Applications Converted From System Center Configuration Manager" -ItemType Folder | Out-Null
            }
            # Import each selected application in to MDT
            Foreach ($mdtApp in $cmAppDTsToConvertToMDTApps ) {
                $mdtAppIndicator++
                Write-Progress -Activity "Processing Application $mdtAppIndicator of $mdtAppCount" -Status $mdtApp.Name -PercentComplete ($mdtAppIndicator / $mdtAppCount * 100)
                Import-MDTApplication -path "DS001:\Applications\$MDTAppFolderName" -enable "True" -Name $mdtApp.Name -ShortName $mdtApp.ShortName -Version $mdtApp.Version -Publisher $mdtApp.Publisher -Language $mdtApp.Language -CommandLine $mdtApp.CommandLine -WorkingDirectory $mdtApp.WorkingDirectory -ApplicationSourcePath $mdtApp.ApplicationSourcePath -DestinationFolder $mdtApp.DestinationFolder
            }
        }
    }
}

Advertisements

There are basically two scenarios when business hours come in to effect in SCCM 2012:

1. A user selects to defer required application/software update installations and reboots to take place outside of business hours:

image

2. A user selects the option in the software center to have SCCM automatically install all required software and restart the computer outside of business hours:image

The default business hours are set to 05:00 – 22:00, Monday to Friday. The business hours can’t be configured through client settings, only by the user or by using the SDK or client WMI methods.

There could be several reasons why you want to change the default business hours. For example, the typical working hours of the majority of your users are different from the default settings and you have a power plan that puts machines in standby before the default business hours end, which would effectively close the window for maintenance outside business hours.

To set the business hours using Compliance settings in ConfigrMgr 2012, navigate to the Asset & Compliance Workspace, select Compliance Settings and create a new configuration item. In the Create Settings page, select a Script setting with a String data type:

image

Edit the Discovery Script and paste in the following Powershell code:

$cmClientUserSettings = [WmiClass]"\\.\ROOT\ccm\ClientSDK:CCM_ClientUXSettings"
$businessHours = $cmClientUserSettings.GetBusinessHours()
$businessHoursCI = [string]$businessHours.StartTime + "," + [string]$businessHours.EndTime + "," + [string]$businessHours.WorkingDays
Return $businessHoursCI

This will return the current business hours as a string in this format:

7,19,62

The first digit is the start time (7am), the second digit is the end time (7pm) and the third digit is the days of the week. The days of the week are calculated using the table below, so Monday – Friday is calculated as 2+4+8+16+32 = 62.

Sunday 1
Monday 2
Tuesday 4
Wednesday 8
Thursday 16
Friday 32
Saturday 64

Edit the Remediation Script and paste in the following code, setting the desired business hours in the variables $startTime, $endTime and $workingDays.


$startTime = 7
$endTime = 19
$workingDays = 62
$cmClientUserSettings = [WmiClass]"\\.\ROOT\ccm\ClientSDK:CCM_ClientUXSettings"
$businessHours = $cmClientUserSettings.PSBase.GetMethodParameters("SetBusinessHours")
$businessHours.StartTime = $StartTime
$businessHours.EndTime = $EndTime
$businessHours.WorkingDays = $WorkingDays

Try {
 $result = $cmClientUserSettings.PSBase.InvokeMethod("SetBusinessHours", $businessHours, $Null)
 If ($result.ReturnValue -eq 0 ) {
 "Success."
 }
 Else {
 "Failed to set SCCM client business hours."
 }
}
Catch {
 "Failed to set SCCM client business hours."
}

Add the following compliance rule, setting the value to your desired business hours and enabling the remediation script for noncompliant clients:

image

This rule will remediate noncompliant clients to the desired business hours.

Lastly, add the configuration item to a configuration baseline and deploy to your target collection.

Now we have centrally controlled business hours in ConfigMgr 2012:

image

Further Reference:

There’s a useful post on Technet that explains the concept of Business Hours vs. Maintenance Windows in System Center 2012 Configuration Manager.

Torsten Merringer has a post here that shows how to set the business hours using VBScript.

Here is a PowerShell script to initiate a task sequence with PowerShell. The script takes one argument – the name of the task sequence. The task sequence must be deployed/advertised to the machine already. It searches the deployed task sequences on the machine and if it finds a task sequence matching the one you specified, it will execute it.

Executing a task sequence this way bypasses the prompt to install a new operating system, which may be useful if you have a task sequence that just performs a user capture or restore, or if you use a task sequence for purposes other than OSD and you don’t want the “install a new operating system” warning dialog.

Install Operating System Warning Dialog

You could potentially use this script to deploy task sequences from the ConfigMgr 2012 Application Catalog. This would give you the benefits of the application model such as dependency and requirement rules as well as the benefits of the Application Catalog with its request/approval system. You could then use the Application Catalog as a request/approval portal for OSD refresh scenarios, something that usually requires a third party tool such as SCCM Expert.

In order to make this work, you would need to deploy an application that runs a script which does the following:

  1. Add the machine to a collection that has the task sequence deployed to it.
  2. Trigger a machine policy evaluation.
  3. Trigger the task sequence.

The application deployment type would need a detection rule to indicate that the task sequence has executed successfully so that the application installation doesn’t interfere with the task sequence. The detection rule could vary depending on what the task sequence does.

I haven’t tried the above and one of the potential issues would be granting the client/script permission to add a machine to a collection. Who knows, maybe Microsoft has plans to allow task sequences to be published to the Application Catalog in a future service pack for ConfigMgr 2012.

I have tested this script with ConfigMgr 2012, but it should with with ConfigMgr 2007 also.


Function Execute-TaskSequence {
    Param (
        [parameter(Mandatory = $true)]
        [string]$Name
    )
    Try {
        Write-Host "Connecting to the SCCM client Software Center..."
        $softwareCenter = New-Object -ComObject "UIResource.UIResourceMgr"
    }
    Catch {
        Throw "Could not connect to the client Software Center."
    }
    If ($softwareCenter) {
        Write-Host "Searching for deployments for task sequence [$name]..."
        $taskSequence = $softwareCenter.GetAvailableApplications() | Where { $_.PackageName -eq "$Name" }
        If ($taskSequence) {
            $taskSequenceProgramID = $taskSequence.ID
            $taskSequencePackageID = $taskSequence.PackageID
            Write-Host "Found task sequence [$name] with package ID [$taskSequencePackageID]."
            # Execute the task sequence
            Try {
                Write-Host "Executing task sequence [$name]..."
                $softwareCenter.ExecuteProgram($taskSequenceProgramID,$taskSequencePackageID,$true)
                Write-Host "Task Sequence executed."
            }
            Catch {
                Throw "Failed to execute the task sequence [$name]"
            }
        }
    }
}

While SCCM 2012 provides user-centric application deployment, it is only possible to deploy software updates to a device or device collection.

Here’s a collection I built in SCCM 2012 to retrieve the primary device of a user who is a member of the “SCCM Software Updates Pilot User Group”. In this way, I can deploy software updates to a device collection based on an Active Directory user group. It’s kind of like user-centric software update deployment.

The query below should create a collection of devices where the device has a primary user that is a member of the AD group “SCCM Software Updates Pilot User Group”.

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 JOIN SMS_UserMachineRelationship ON SMS_R_System.Name=SMS_UserMachineRelationship.ResourceName JOIN SMS_R_User ON SMS_UserMachineRelationship.UniqueUserName=SMS_R_User.UniqueUserName WHERE SMS_UserMachineRelationship.Types=1 AND SMS_R_User.UserGroupName="DOMAIN\\SCCM Software Updates Pilot User Group"

I’m using Configuration Manager 2012 Beta 2 with MDT 2012 Beta 1 to create task sequences for OSD. This error bugged me for days before I found a fix. The error code 0x80004005 is apparently an access denied error. Most of the forums on this error point to certificate issues or permissions issues. Having checked the certificates and permissions, re-created the task sequence, re-created packages and re-distributed everything I was still getting the same error.

image

The SMSTS.LOG file looks like this:

<![LOG[Executing Policy Body Request.]LOG]!><time="09:13:02.784-60" date="07-07-2011" component="TSMBootstrap" context="" type="0" thread="1064" file="tspolicy.cpp:2010">
<![LOG[Using Authenticator for policy]LOG]!><time="09:13:02.784-60" date="07-07-2011" component="TSMBootstrap" context="" type="1" thread="1064" file="libsmsmessaging.cpp:4448">
<![LOG[CLibSMSMessageWinHttpTransport::Send: URL: SCCMServer:80  GET /SMS_MP/.sms_pol?ScopeId_647BE467-0336-4ECD-9411-6BEDD13345A/AuthList_4b3c67e6-43bb-4f8d-8362-3504308e3452/VI.SHA256:GHFSD3456DHDSGH91FE949D8A0445EFA08701D81C984564]LOG]!><time="09:13:02.784-60" date="07-07-2011" component="TSMBootstrap" context="" type="1" thread="1064" file="libsmsmessaging.cpp:8720">
<![LOG[Request was succesful – 200]LOG]!><time="09:13:02.877-60" date="07-07-2011" component="TSMBootstrap" context="" type="0" thread="1064" file="libsmsmessaging.cpp:9046">
<![LOG[cannot load compressed XML policy]LOG]!><time="09:13:04.018-60" date="07-07-2011" component="TSMBootstrap" context="" type="3" thread="1064" file="libsmsmessaging.cpp:4617">
<![LOG[oPolicy.RequestPolicy((GetPolicyFlags() & POLICY_SECURE) != 0, (GetPolicyFlags() & POLICY_COMPRESS) != 0), HRESULT=80004005 (e:\nts_sccm_retail\sms\framework\tscore\tspolicy.cpp,2014)]LOG]!><time="09:13:04.018-60" date="07-07-2011" component="TSMBootstrap" context="" type="0" thread="1064" file="tspolicy.cpp:2014">
<![LOG[Failed to download policy ScopeId_647BE467-0336-4ECD-9411-6BEDD13345A/AuthList_4b3c67e6-43bb-4f8d-8362-3504308e3452/VI (Code 0x80004005).]LOG]!><time="09:13:04.018-60" date="07-07-2011" component="TSMBootstrap" context="" type="3" thread="1064" file="tspolicy.cpp:2014">
<![LOG[DownloadPolicyBody(), HRESULT=80004005 (e:\nts_sccm_retail\sms\framework\tscore\tspolicy.cpp,2100)]LOG]!><time="09:13:04.018-60" date="07-07-2011" component="TSMBootstrap" context="" type="0" thread="1064" file="tspolicy.cpp:2100">

I used the free text search feature in SCCM to search all objects for the string after AuthList_ 4b3c67e6-43bb-4f8d-8362-3504308e3452

The search returned 2 related objects:

A configuration Item in Assets and Compliance.

A software update group in the Software Library.

Checking the properties of these, it appeared that there was no security scope set on the configuration item. When I tried to set the security scope, Config Mgr crashed. I looked in Assets and Compliance and the item wasn’t there. So I ran the search again and deleted the item from the search results. The item was applied to a collection that included the machine running the TS. When I re-ran the task sequence, the error was gone! It goes to show how something seemingly unrelated to the task sequence can cause problems with it.

Some more SCCM 2012 gotcha’s here:

http://scug.be/blogs/nico/archive/2011/04/26/sccm-2012-odd-issues.aspx