Overview

The purpose of this article is to share some snippets of code for handling the individual steps involved with bringing a domain-joined, member server up to being a System Center Configuration Manager 1511 Primary Site Server.  The genesis of this experiment was a wager, but I won’t say what was placed on the table.  I’ve been beating on this script for several weeks and it works very well for my needs.  But I just now had a few minutes to post it on my blog.  And aside from that, I’m overdue for an excursion into nerdville.  I hope it was all worth it.

marie-wilson-cooking

The Lab Setup

The starting point I used in my lab is a member server running Windows Server 2012 R2, with 4 GB memory, a C: drive, and an E: drive.  The domain is contoso.com, with a single domain controller named DC1, and a member file server named FS1.

FS1 has a share named “Apps” with a folder named “MS”, which contains sub-folders for the installation media, the PowerShell script and supporting files.

Beneath the \\FS1\Apps\MS share, I created sub-folders “ADK10”, “CMToolkit”, “MDT2013u2”, “SCCM1511” and “SQL2014”.

The ADK10 folder is populated with the downloaded offline installation files for installing Windows 10 ADK.  The folder contains adksetup.exe, the UserExperienceManifest.xml file and the Installers folder.

The CMToolkit folder contains ConfigMgrTools.msi.

The MDT2013u2 folder contains the file “MicrosoftDeploymentToolkit2013_x64.msi” which includes Update 2.

The SCCM1511 folder contains the extracted installation media for ConfigMgr 1511, including the splash.hta file.  I added a sub-folder named “PreReqs” which contains the downloaded prerequisites using setupdl.exe.

The SQL2014 folder contains the extracted installation media for SQL Server 2014 with Service Pack 1.

Notes

  1. I’m not focused on validating server resources.  That is done separately and should be part of the server build process.  If you’re not sure how to configure the baseline, refer to Kent Agerlund’s blog post about using a spreadsheet to estimate resource requirements.
  2. The domain “contoso.com” has a user account named “sccmadmin”, which for this lab is a member of Domain Admins and Schema Admins.
  3. In addition to the “Apps” share on FS1, I created a “sources” share which contains the Features on Demand (i.e. sxs) content for Windows Server 2012 R2 Standard Edition.  This helps insure roles and features get installed without errors due to missing installation components.
  4. The form of each line of code, the methodology I chose, and the use of functions, pipelines, parameters, and so on, are entirely arbitrary.  Do not assume this is the “only way” to accomplish this sort of exercise.  I’m sure it could be done more efficiently and probably more apropos.
  5. This is intended for use by those who are somewhat familiar with performing an SCCM site server installation using traditional methods, including all of the prerequisite components and configuration settings.
  6. I do not provide all of the code for this process end-to-end.  Some of it I chose to leave out, but I will explain what it needs to do.  Functions which are not shown are highlighted in a different color.  Consider those items as homework. 🙂
  7. Once SCCM is fully installed, and the PowerShell cmdlet modules are available, this approach can be extended to build out the rest of the site hierarchy settings if desired.
  8. Before you start lecturing me on the use of Write-Host, please just shut up.  In the actual script I use a custom Write-Log function to handle things.  Write-Host is just for example.
  9. All of this code is provided without warranty or guarantee of any kind for any purpose whatsoever.  Always test in a non-production environment.  Use at your own risk.

The Process

The workflow sequence is as follows (some steps may be out of order, but the order is really controlled by how the functions are called in the script.

  1. Establish a shared repository for installation media (described earlier)
  2. Edit the settings.xml file to suit your environment.
  3. Import the configuration settings from settings.xml
  4. Define common variables, and functions
  5. Create AD service-related user accounts and group memberships
  6. Install required Windows Server roles and features (except for WSUS)
  7. Install Windows 10 ADK
  8. Install SQL Server 2014
  9. Install WSUS role
  10. Install MDT 2013 Update 2
  11. Extend the AD Schema
  12. Create the “System Management” container
  13. Install SCCM 1511 as primary site server, without a DP role
  14. Install ConfigMgr Tools

The Ingredients

The computers:

  • DC1 = domain controller for contoso.com
  • FS1 = file server, with “Apps” share
  • P01 = member server (will become SCCM site server)

User accounts:

  • sccmadmin = member of Domain Admins, Schema Admins
  • svc-sccm = member of Domain Admins, SQL Admins

Security Groups:

  • SCCM Admins = group which is added to SCCM site to have full admin rights.
  • SCCM Site Servers = used for aggregating permissions delegation on the AD “system management” container.  P01 is already a member.
  • SQL Admins = group which is added into SQL administrators role

The Files

For this example, I break this down into the following files.  These are stored under “\\FS1\Apps\Scripts”.

  • New-CMSiteServer.ps1
  • settings.xml
  • _roles.ini
  • _CmTemplate.ini
  • _SqlTemplate.ini
  • sql_memory.sql
  • psexec.exe

Explanation of the Files:

  • New-CMSiteServer.ps1 = the main script.
  • settings.xml = contains the parameters used to control the execution of the PowerShell script.
  • _CmTemplate.ini = is a generic unattended installation INI file for use with SCCM 1511.  Some values are replaced with %variable% entries.
  • _SqlTemplate.ini = is a generic unattended installation INI file for use with SQL Server 2014.  Some values are replaced with %variable% entries.
  • _roles.ini = a list of Windows Feature codes which is imported by the PowerShell script to add the required roles and features.
  • sql_memory.sql = a T-SQL script which is executed via sqlcmd.exe to perform post-installation configuration changes of the SQL Server instance on P01.
  • psexec.exe = the venerable and renown remote process invocation utility by Sysinternals.  This is used by the PowerShell script to execute the schema extension on domain controller DC1 from the session on P01.

Setup Preparation

  1. Download all of the installation media required for this process.  This includes SQL Server 2014, MDT 2013 Update 2 (or whatever is the latest), Windows 10 ADK (latest supported version), and Configuration Manager 1511.
  2. Create a shared location within your network to host the installation media content.  I recommend a single root folder, such as “MS” or “Microsoft” and add a folder for each product below that.  For example, “ADK10”, “MDT2013u2”, “SQL2014” and “SCCM1511”.
  3. Create the pre-download content share for installing ADK. It should result in a sub-folder named “Installers” along with adksetup.exe and a manifest XML file.  The easiest way to do this is run the adksetup.exe and choose the second option to download content only…
    ps_sccm_adk
  4. Create the prerequisites folder and download the prerequisites for SCCM 1511 installation ahead of time.  I recommend using a sub-folder beneath the SCCM1511 folder, such as “PreReqs”.  Then use the setupdl.exe utility from the installation media (smssetup\bin\x64\setupdl.exe) to download the files to the new folder.
    ps_sccm_setupdl
  5. Download the ConfigMgrTools.msi from here.  Save it under the CMtools folder below the shared source location.
  6. Create a shared sources location on your network for Windows Server 2012 R2.  Read this.
  7. Drink Coffee.  Lots and lots of it.  Pour it over your body and rub it in gently.  Chew on coffee beans.  Put on your t-shirt that say “I don’t do shit without my coffee!”.

Preparing the Script Data Files

Edit the settings.xml file using any standard text or code editor.  I used Notepad++, but that’s not required.

Change “ContentSource” to refer to the root share.  For example, if you host the installation media under \\Server3\Software\Microsoft, then change the assigned value to suit (e.g. <ContentSource x=”\\Server3\Software\Microsoft” />

Edit the two INI template files to enter the appropriate Produce Key for each product (SCCM 1511 and SQL 2014).  If you’re using an MSDN license of SQL Server, you might be okay with leaving the assignment blank (e.g. PID=”” )

Special Note about the SQL INI file:

You can include the service accounts and their passwords within the file, if you wish.  I chose to control the user account names via the settings.xml file, and feed the respective passwords in during runtime.  The way I built the script is that it creates the new INI within the source folder with the rest of the SQL Server installation content.  I didn’t want the account passwords hanging out there, so I embed those in the settings.xml where the PowerShell script resides.  Like I said, it’s not required, and you may have a better way to suit your needs.

Processing the INI templates:

The code I developed imports the INI template content into memory (variable assignment), then matches up the imported settings from settings.xml to perform a word substitution on each matching variable.  The updated content is then written out to a new “custom.ini” file within the installation source folder for each product (SCCM and SQL Server).

For example, in the _SqlTemplate.ini file, there’s a line INSTALLSHAREDDIR=”%SQLInstallPath%”.  During runtime, the PowerShell script fetches the <SQLInstallPath> key assignment, the replaces the “%SQLInstallPath%” entry, before writing the new INI file.  If settings.xml has E:\SQL as the assigned value, when you view the resulting “custom.ini” file under the SQL Server installation source folder, it will contains INSTALLSHAREDIR=”E:\SQL”.

_template.ini –> PowerShell Script (merge from settings.xml) –> custom.ini

If you prefer hard-coding some parameters in the INI files, edit the templates to remove the desired %variable% entries and replace them with your preferred value.  The PowerShell script will then ignore those entries.

New-CMSiteServer.ps1

Not shown.  However, the guts are ripped open and put on display here (except where highlighted otherwise).

settings.xml

<base>
 <common>
  <ContentSource x="\\FS1.contoso.com\Apps\MS" />
  <AltWinSource x="\\FS1.contoso.com\Sources\sxs" />
  <DCHostName x="DC1.contoso.com" />
 </common>
 <sccm>
  <CMSiteType x="PrimarySite" />
  <CMSiteCode x="PS1" />
  <CMSiteName x="Primary Site 1" />
  <CMSiteServer x="P01.contoso.com" />
  <CMSiteServerName x="P01" />
  <CMSiteDomain x="contoso.com" />
  <CMSiteAdminAccount x="contoso\sccmadmin" />
  <CMSourcePath x="$ContentSource\SCCM1511" />
  <CMTargetPath x="E:\ConfigMgr" />
  <CMPreReqsSource x="$ContentSource\SCCM1511\PreReqs" />
  <CMProductKey x="12345-12345-12345-12345-12345" />
  <CMSQLDataPath x="E:\MSSQL" />
  <CMAdminConsole x="1" />
  <CMDPServer x="" />
  <ExtDaSchPath x="$ContentSource\SCCM1511\SMSSETUP\BIN\x64\extadsch.exe" />
 </sccm>
 <adk>
  <ADKSource x="$ContentSource\ADK10" />
  <ADKTarget x="E:\ADK10" />
 </adk>
 <mdt>
  <MDTSource x="$ContentSource\MDT2013u2" />
  <MDTTarget x="E:\MDT2013" />
 </mdt>
 <sql>
  <SQLSource x="$ContentSource\MS\SQL2014" />
  <SQLInstall x="C:\MSSQL2014" />
  <SQLDataPath x="E:\MSSQL" />
  <SQLInstance x="MSSQLSERVER" />
  <SQLProductKey x="12345-12345-12345-12345-12345" />
  <SQLAdminUser x="contoso\sccmadmin" />
  <SQLAdminGroup x="contoso\SQL Admins" />
  <SQLAgentAccount x="contoso\svc-sccm,Password$123" />
  <SQLSvcAccount x="contoso\svc-sccm,Password$123" />
  <SQLRepAccount x="contoso\svc-sccm,Password$123" />
 </sql>
 <wsus>
  <WSUSTarget x="E:\WSUS" />
 </wsus>
 <accounts>
  <UserOU x="CORP\ServiceAccounts" />
  <UserAccount x="svc-sccm,SCCM SQL Server Account,Domain Admins|SQL Admins,Password$123" />
  <UserAccount x="cmdomjoin,SCCM Domain Join Account,Domain Admins,Password$123" />
  <UserAccount x="cmclient,SCCM Client Install Account,Domain Admins,Password$123" />
 </accounts>
</base>

_CmTemplate.ini

[Identification]
Action=Install%CMSiteType%

[Options]
ProductID=%CMProductKey%
SiteCode=%CMSiteCode%
SiteName=%CMSiteName%
SMSInstallDir=%CMTargetPath%
SDKServer=%CMSiteServer%
RoleCommunicationProtocol=HTTPorHTTPS
ClientsUsePKICertificate=0
PrerequisiteComp=0
PrerequisitePath=%CMPreReqsSource%
ManagementPoint=%CMSiteServer%
ManagementPointProtocol=HTTP
DistributionPoint=%CMDPServer%
DistributionPointProtocol=HTTP
DistributionPointInstallIIS=1

AdminConsole=%CMAdminConsole%
JoinCEIP=0
MobileDeviceLanguage=0

[SQLConfigOptions]
SQLServerName=%CMSiteServer%
DatabaseName=CM_%CMSiteCode%
SQLSSBPort=4022
SQLDataFilePath=%SQLDataPath%
SQLLogFilePath=%SQLDataPath%

[HierarchyExpansionOption]
CCARSiteServer=

_SqlTemplate.ini

[OPTIONS]
ACTION="Install"
PID="%SQLProductKey%"
ENU="True"
QUIET="True"
UpdateEnabled=1
ERRORREPORTING=0
USEMICROSOFTUPDATE=0
FEATURES=SQLENGINE,RS,SSMS,ADV_SSMS
UpdateSource="%SQLSource%\Updates"
HELP="False"
INDICATEPROGRESS="False"
X86="False"
INSTALLSHAREDDIR="%SQLInstallPath%"
INSTALLSHAREDWOWDIR="%SQLInstallPath%x86"
INSTANCENAME="%SQLInstance%"
SQMREPORTING="False"
INSTANCEID="%SQLInstance%"
RSINSTALLMODE="DefaultNativeMode"
INSTANCEDIR="%SQLInstallPath%"
AGTSVCACCOUNT="%SQLAgentAccount%"
AGTSVCSTARTUPTYPE="Automatic"
COMMFABRICPORT="0"
COMMFABRICNETWORKLEVEL="0"
COMMFABRICENCRYPTION="0"
MATRIXCMBRICKCOMMPORT="0"
SQLSVCSTARTUPTYPE="Automatic"
FILESTREAMLEVEL="0"
ENABLERANU="False"
SQLCOLLATION="SQL_Latin1_General_CP1_CI_AS"
SQLSVCACCOUNT="%SQLSvcAccount%"
SQLSYSADMINACCOUNTS="%SQLAdminGroup%" "%SQLAdminUser%"
INSTALLSQLDATADIR="%SQLDataPath%"
ADDCURRENTUSERASSQLADMIN="False"
TCPENABLED="1"
NPENABLED="0"
BROWSERSVCSTARTUPTYPE="Disabled"
RSSVCACCOUNT="%SQLRepAccount%"
RSSVCSTARTUPTYPE="Automatic"

_roles.ini

BITS
RDC
AS-WAS-Support
AS-Net-Framework
Net-Framework-Core
Net-Http-Activation

; .NET 4.5
AS-Net-Framework
NET-WCF-HTTP-Activation
NET-Framework-45-Feature

Web-Default-Doc

; COMMON HTTP
Web-Dir-Browsing
Web-Http-Errors
Web-Static-Content
Web-Http-Redirect

; HEALTH AND DIAGNOSTICS
Web-Http-Logging
Web-Log-Libraries
Web-Request-Monitor
Web-Http-Tracing

; PERFORMANCE
Web-Stat-Compression
Web-Dyn-Compression

; SECURITY
Web-Filtering
Web-Basic-Auth
Web-IP-Security
Web-Url-Auth
Web-Windows-Auth

; APP DEVELOPMENT
Web-Net-Ext
Web-Net-Ext45
Web-Asp-Net
Web-Asp-Net45

; MANAGEMENT
Web-Mgmt-Tools
Web-Mgmt-Console
Web-Mgmt-Compat
Web-Metabase
Web-Lgcy-Mgmt-Console
Web-Lgcy-Scripting
Web-WMI
Web-Scripting-Tools
Web-Mgmt-Service

Get Ready.  Get Set…

$ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition

Load operational settings

[xml] $settings = Get-Content "$ScriptPath\settings.xml"
if ($settings.ChildNodes.Count -lt 1) {
  Write-Error "failed to load settings"
  Exit
}
[string] $RootShare = $settings.base.common.ContentSource.x

Common Functions

<#
pull the [x] element value string and replace nested variables with expanded strings.
#>
function Get-Setting {
  param (
    [parameter(Mandatory=$True)]
    [System.Xml.XmlElement] $KeyPath
  )
  $val = ($KeyPath.x).ToString().Replace("`$ContentShare", $RootShare)
  return $val
}

<#
import a template INI, substitute nested variables with values from settings.xml, create a new INI in the target folder to use for unattended installs.
#>

function New-SetupINI {
  param (
    [parameter(Mandatory=$True)] 
    [string] 
    [validateSet('CM','SQL')] $ProductCode
  )
  if ($ProductCode -eq 'CM') {
    $KeySet = $settings.base.sccm
    $SourcePath = Get-Setting "CMSourcePath"
  }
  else {
    $KeySet = $settings.base.sql
    $SourcePath = Get-Setting "SQLSource"
  }
  $Template = ("_$ProductCode"+"template.ini")
  $temp = Get-Content "$ScriptPath\$Template"
  $NewINI = "$SourcePath\custom.ini"
  $new = $temp
  foreach ($k in $KeySet.ChildNodes) { 
    $KeyName = $k.LocalName
    $KeyVal = $k.x
    if ($KeyName -like "SQL*Account) {
      $KeyVal = $KeyVal.Split(',')[0]
    }
    else {
      $KeyVal = Get-Setting "$KeyName"
    }
    if ($KeyVal -eq $null) {
      $KeyVal = ""
    }
    $new = $($new.Replace("%$KeyName%", $KeyVal)).Replace("`$ContentSource", $RootShare)
  }
  if (($new -ne $null) -and ($new -ne "")) {
    $out = $true
    $new | Set-Content $NewINI
  }
  else {
    $out = $false
  }
  return $out
}
<#
fetch value assigned to key from settings.xml after it has been loaded into $settings.
#>

function Get-Setting {
  param (
    [parameter(Mandatory=$True)]
    [string] $Key
  )
  $val = $($Settings.GetElementsByTagName("$Key").x)
  if ($val -ne $null) {
    $val = $val.ToString().Replace("`$ContentSource", $RootShare)
  }
  return $val
}


Create AD Service Accounts

<#
following section checks for user account and creates it if not found.
#>
function Create-ServiceAccounts {
  $out = $True
  $users = $settings.base.accounts.UserAccount.x
  $userOU = $settings.base.accounts.UserOU.x
  $userOU = LDAP-Path $userOU
  $out = $True
  foreach ($uset in $users) {
    $umap = $uset.Split(',')
    $userName = $umap[0]
    $userDesc = $umap[1]
    $Groups = $umap[2]
    $userPwd = $umap[3]
    if (Test-User $userName) {
      Write-Host "account already exists: $userName"
    }
    else {
      Write-Host "account not found: $userName"
      New-ADUser -SamAccountName $userName -Name $userName -Description $userDesc -Path "$UserOU" -PassThru |
        Set-ADAccountPassword -Reset -NewPassword (ConvertTo-SecureString -AsPlainText $userPwd -Force) -PassThru | 
          Enable-ADAccount
      if (Test-User $userName) {
        Write-Host "account created: $userName"
      }
      else {
        Write-Error "failed to create account: $userName"
        $out = $False
      }
    }
    if ($out -eq $True) {
      if ($Groups -ne "") {
        foreach ($g in $Groups.Split('|')) {
          Add-ADGroupMember "$g" "$userName"
        }
      }
    }
  }
  return $out
}

Add Windows Server Roles and Features

function Add-ServerRoles {
  $out = $True
  $AltSource = Get-Setting "AltWinSource"
  $roles = Get-Content "$ScriptPath\_roles.ini" |
    Where {($_ -notlike ";*") -and ($_ -ne "")}
  if (($roles -ne $null) -and ($roles.Length -gt 0)) {
    if (Test-Path $AltSource) {
      $roles |
        Install-WindowsFeature -IncludeAllSubFeature -IncludeManagementTools -Source $AltSource
    }
    else {
      $roles |
        Install-WindowsFeature -IncludeAllSubFeature -IncludeManagementTools
    }
    foreach ($role in $roles) {
      if ((Get-WindowsFeature $role | Select-Object -ExpandProperty Installed) -ne $True) {
        $out = $False
      }
    }
  }
  return $out
}

Install Windows 10 ADK

function Install-ADK {
  $out = $True
  $RegPath = "HKLM:SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{7115AD09-8559-7415-9A13-B4DC9CECC3E7}"
  if (Test-Path $RegPath) {
    Write-Host "ADK 10 is already installed"
  }
  else {
    $ADKSource = $settings.base.adk.adksource.x + "\adksetup.exe"
    $ADKTarget = $settings.base.adk.adktarget.x
    $Features  = "OptionId.DeploymentTools OptionId.WindowsPreinstallationEnvironment OptionId.UserStateMigrationTool"
    $ADKargs = "/installpath `"$ADKTarget`" /features $Features /quiet /norestart"
    if (Test-Path $ADKSource) {
      $proc = Start-Process $ADKSource -ArgumentList $ADKArgs -NoNewWindows -PassThru
      $hand = $proc.handle
      $proc.WaitForExit()
      $exit = $proc.ExitCode
      if ($exit -eq 0) {
        $out = $True
      }
      else {
        Write-Error "install failed with exit code: $exit"
        $out = $False
      }
    }
    else {
      Write-Error "installation source file not found"
      $out = $False
    }
  }
  return $out
}

Install SQL Server 2014

function Install-SQLServer {
  $out = $True
  $RegPath = "HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{17531BCD-C627-46A2-9F1E-7CC920E0E94A}"
  if (Test-Path $RegPath) {
    Write-Host "SQL Server 2014 is already installed"
  }
  elseif (Test-Path "HKLM:SOFTWARE\Microsoft\MSSQLServer\CurrentVersion") {
    $check = (Get-ItemProperty "HKLM:SOFTWARE\Microsoft\MSSQLServer\MSSQLServer\CurrentVersion").CurrentVersion 
    Write-Host "SQL Server installation found (build: $check)"
    $out = $False
  }
  else {
    Write-Host "No version of SQL Server was found."
 
    $SQLSource = Get-Setting "SQLSource"
    $SQLTarget = Get-Setting "SQLInstallPath"
    $SQLDataPath = Get-Setting "SQLDataPath"
    if (New-SetupINI 'SQL') {
      $CFGfile = "$SQLSource\custom.ini"
      if (Test-Path $CFGFile) {
        if (Test-NetFramework35) {
          $pwd1 = (Get-Setting "SQLSvcAccount").Split(',')[1]
          $pwd2 = (Get-Setting "SQLAgentAccount").Split(',')[1]
          $pwd3 = (Get-Setting "SQLRepAccount").Split(',')[1]
          $SQLExe = "$SQLSource\setup.exe"
          $SqlArgs = "/Q /SQLSVCPASSWORD=$pwd1 /AGTSVCPASSWORD=$pwd2 /RSSVCPASSWORD=$pwd3 /IAcceptSQLServerLicenseTerms /ConfigurationFile=$CfgFile"

          $proc = Start-Process "$SQLSource\setup.exe" -ArgumentList $sqlargs -NoNewWindow -PassThru
          $hand = $proc.Handle
          $proc.WaitForExit()
          $exit = $proc.ExitCode
          if ($exit -eq 0) {
            Write-Host "installation completed successfully."
          }
          else {
            Write-Error "installation failed with exit code: $exit"
            $out = $False
          }
        }
        else {
          Write-Error "unable to install .NET 3.5"
          $out = $False
        }
      }
      else {
        Write-Error "failed to locate custom.ini"
        $out = $False
      }
    }
    else {
      Write-Error "failed to compile unattend configuration INI file."
      $out = $False
    }
  }
  return $out
}

Configure SQL Memory Limits

Note: build and test your SQL script first.  This example calls it “sql_memory.sql”.  Then executed it from PowerShell with something like the following:

function Config-SQLMemory {
  Write-Host "adjusting SQL Server memory limits..."
  $out = $True
  $SqlHost = Get-Setting "CMSiteServer"
  $SQLScript = "$ScriptPath\sql_memory.sql"
  $SQLlog = "$ScriptPath\sql_memory_change.txt"
  $SQLcmd = "-S $SqlHost -i $SQLScript -o $SQLlog"
  $proc = Start-Process "sqlcmd.exe" -ArgumentList $SQLcmd -NoNewWindow -PassThru
  $hand = $proc.handle
  $proc.WaitForExit()
  $exit = $proc.ExitCode
  if ($exit -eq 0) {
    Write-Host "memory settings have been adjusted."
  }
  else {
    Write-Error "failed with exit code: $exit"
    $out = $False
  }
  return $out
}

The SQL script should follow the recommended approach to allocating memory to SQL Server versus the OS and other resources.  The 80/20 rule is pretty common.  Again, refer to Kent’s blog post about this.

Install WSUS

function Install-WSUS {
  $out = $True
  if ((Get-WindowsFeature UpdateServices-DB | Select-Object -ExpandProperty InstallState) -eq 'InstallPending') {
    Write-Host "WSUS is already installed, but needs a restart."
  }
  elseif ((Get-WindowsFeature UpdateServices-DB | Select-Object -ExpandProperty InstallState) -eq 'Installed') {
    Write-Host "WSUS is already installed."
  }
  else {
    Write-Host "Installing WSUS role..."
    $AltSource = Get-Setting "AltWinSource"
    $WSUSFolder = Get-Setting "WSUSTarget"
    $ServerName = Get-Setting "CMSiteServer"
    if (Test-Path $AltSource) {
      Install-WindowsFeature UpdateServices-Services,UpdateServices-DB -IncludeAllSubFeature -IncludeManagementTools -Source $AltWinSource
    }
    else {
      Install-WindowsFeature UpdateServices-Services,UpdateServices-DB -IncludeAllSubFeature -IncludeManagementTools
    }
    if ((Get-WindowsFeature UpdateServices-DB | Select-Object -ExpandProperty InstallState) -eq 'Installed') {
      Write-Host "WSUS role installed successfully."
    }
    else {
      Write-Error "WSUS role installation failed."
      $out = $False
    }
  }
  return $out
}

Install MDT 2013 Update 2

function Install-MDT {
  $out = $True
  $RegPath = "HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{F172B6C7-45DD-4C22-A5BF-1B2C084CADEF}"
  if (Test-Path $RegPath) {
    Write-Host "MDT 2013 update 2 is already installed."
  }
  else {
    $MDTSource = (Get-Setting "MDTSource") + "\MicrosoftDeploymentToolkit2013_x64.msi"
    $MDTTarget = Get-Setting "MDTTarget"
    $MDTArgs = "/i `"$MDTSource`" INSTALLDIR=`"$TargetPath`" ALLUSERS=1 /qn /norestart"
    if (Test-Path $MDTSource) {
      $proc = Start-Process "msiexec.exe" -ArgumentList $MDTArgs -NoNewWindow -PassThru
      $hand = $proc.Handle
      $proc.WaitForExit()
      $exit = $proc.ExitCode
      if ($exit -eq 0) {
        Write-Host "MDT 2013 installation completed successfully."
      }
      else {
        Write-Error "Installation failed with exit code: $exit"
        $out = $False
      }
    }
    else {
      Write-Error "installation source files not found."
      $out = $False
    }
  }
  return $out
}

Extend the AD Schema

Note: this requires the script to be executed under the context of a user account which is a member of both Domain Admins and Schema Admins.  It is run from the soon-to-be SCCM site server, not on the domain controller.   The DC is identified in settings.xml as DCHostName.  The ExtDaSch.exe file is identified in settings.xml as ExtdaSchPath.  The script copies the extdasch.exe file to the C$ share on the specified DC and executes it remotely using PsExec.exe.

function Test-SchemaExtended {
  $SchPath = (Get-ADRootDSE).schemanamingContext
  $att = Get-ADObject -Filter * -searchbase $SchPath -Properties * | Where Name -eq "MS-SMS-Site-Code"
  if ($att) { 
    return $True 
  }
  else {
    return $False
  }
}

function Extend-ADSchema {
  if (Test-SchemaExtended) {
    Write-Host "schema has already been extended."
    $out = $True
  }
  else {
    Write-Host "schema has not been extended."
    if (Install-ADPowerShell) {
      $DCHostName = Get-Setting "DCHostName"
      $dcname = $DCHostName.Split('.')[0]
      $ExtSource = Get-Setting "ExtDaSchPath"
      if (!(Test-Path "\\$dcname\c$\extadsch.exe")) {
        Copy-Item -Path "$ExtSource" -Destination "\\$dcname\C$\Extadsch.exe"
      }
      if (!(Test-Path "\\$dcname\c$\extadsch.exe")) {
        Write-Error "file failed to copy."
        $out = $False
      }
      else {
        Write-Host "invoking: $ScriptPath\psexec.exe -accepteula \\$dcname c:\extdasch.exe"
        Start-Process "$ScriptPath\psexec.exe" -ArgumentList "\\$dcname c:\extadsch.exe -accepteula" -Wait
        $ExtLog = "\\$dcname\c$\extdasch.log"
        if (Test-Path $ExtLog) {
          $txtData = Get-Content -Path $ExtLog | Where {$_.Contains("Successfully extended")}
          if ($txtData -ne $null) {
            $out = $True 
          }
          else {
            $out = $False
          }
        }
        else {
          Write-Warning "output log was not found."
          Write-Warning "AD schema might not have been extended."
          $out = $False
        }
      }
    }
    else {
      Write-Error "failed to load RSAT-AD-PowerShell tools."
      $out = $False
    }
  }
  return $out 
}

Create the Systems Management Container

function New-Container {
  $out = $True
  $ContName = "System Management"
  $DomainDN = ((Get-ADDomain).DistinguishedName)
  $Container = "ad:CN=$ContName,CN=System,$DomainDN"
  $SiteGroup = Get-Setting "CMSiteServersGroup"
  $CmSiteServerName = Get-Setting "CMSiteServerName"
  $GroupName = "$($env:USERDOMAIN)\$SiteGroup"
  if (Test-Path $Container) {
    Write-Host "AD container already exists"
  }
  else {
    Write-Host "Creating the new Container object..."
    New-ADObject -Name $ContName -Type Container -Path ("CN=System," +$DomainDN)
  }
  if (Test-Path $Container) {
    if (Test-Path "dsacls.exe") {
      $DSArgs = " `"CN=$ContName,CN=System,$DomainDN`" /I:T /G `"$GroupName`:GA`""
      Write-Host "delegating permissions on the container to $SrvName..."
      $proc = Start-Process "dsacls.exe" -ArgumentList $DSArgs -NoNewWindow -PassThru
      $hand = $proc.Handle
      $proc.WaitForExit()
      $exit = $proc.ExitCode
      if ($exit -eq 0) {
        Write-Host "Container was created."
      }
      else {
        Write-Error "failed to delegate permissions, exit code: $exit"
        $out = $True
      }
    }
    else {
      Write-Error "dsacls.exe is missing!"
      $out = $False
    }
  }
  else {
    Write-Error "failed to create new container."
    $out = $False
  }
  return $out
}

Install SCCM 1511

Keep in mind that, like the SQL installation phase, this takes a while.  The setup.exe payload process first (optionally) runs the prerequisite checker.  If that passes, it then begins downloading, caching and queuing up the installation tasks.  Watch the log files and the target folder and you should see it come alive within 5 minutes of this part being called.

function Install-SCCM {
  $out = $True
  $RegPath = "HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\SMS Primary Site"
  if (Test-Path "$RegPath") {
    if ((Get-ItemProperty -Path "$RegPath" | Select -ExpandProperty DisplayVersion) -ge "5.00.8325.1000") {
      Write-Host "SCCM 1511 or newer is already installed."
    }
  }
  else {
    if (Test-NetFramework35) {
      Write-Host "Installing SCCM 1511..."
      $CMSource = Get-Setting "CMSourcePath"
      $CMSetup  = "$CMSource\SMSSETUP\BIN\x64\setup.exe"
      $CMTarget = Get-Setting "CMInstallPath"
      $CMPreReq = Get-Setting "CMPreReqsSource"
      if (New-SetupINI 'CM') {
        $CfgFile = "$CMSource\custom.ini"
        if (Test-Path $CfgFile) {
          $CMargs = "/script $CfgFile"
          $proc = Start-Process "$CMSetup" -ArgumentList $CMargs -NoNewWindow -PassThru
          $hand = $proc.Handle
          $proc.WaitForExit()
          $exit = $proc.ExitCode
          if ($exit -eq 0) {
            Write-Host "SCCM 1511 installation completed."
          }
          else {
            Write-Error "SCCM 1511 installation failed with exit code: $exit"
            $out = $False
          }
        }
        else {
          Write-Error "failed to locate custom installation INI file."
          $out = $False
        }
      }
      else {
        Write-ERROR "failed to compile new custom installation INI file."
        $out = $False
      }
    }
    else {
      Write-Error "unable to install without .NET 3.5."
      $out = $False
    }
  }
  return $out
}

Install ConfigMgr Tools

This is pretty much the same as MDT since it’s an MSI package.  Just call it with the basic parameters (e.g. /qn /norestart) and it works fine.

function Install-CMToolkit {
  $out = $True
  $RegPath = "HKLM:SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{4FFF7ECC-CCF7-4530-B938-E7812BB91186}"
  if (Test-Path "$RegPath") {
    if ((Get-ItemProperty -Path $RegPath | Select -ExpandProperty DisplayVersion) -ge "5.00.7958.1151") {
      Write-Host "ConfigMgr Toolkit 2012 R2 or newer is already installed."
    }
  }
  else {
    $ToolSource = (Get-Setting "CMToolkit") + "\ConfigMgrTools.msi"
    if (Test-Path $ToolSource) {
      Write-Host "installing ConfigMgr Toolkit..."
      $proc = Start-Process msiexec.exe -ArgumentList "/i $ToolSource /qn /norestart" -NoNewWindow -PassThru
      $hand = $proc.handle
      $proc.WaitForExit()
      $exit = $proc.ExitCode
      if ($exit -eq 0) {
        Write-Host "installation completed successfully."
      }
      else {
        Write-Error "installation failed with exit code: $exit"
        $out = $False
      }
    }
    else {
      Write-Error "unable to locate ConfigMgr Toolkit installer."
      $out = $false
    }
  }
  return $out
}

Closing Thoughts

As I’ve already said, this is just ONE possible way to do this. I’m sure if you’re familiar with the process (or more familiar with it than I) and adept with PowerShell programming, you can do much better.  Hopefully this was of some help to someone.

Some improvements I’ve made in the source script include GUID assignments in the settings.xml file to identify the individual apps (rather than hard-coding in the script), and breaking out user account processing in a separate XML (or at least fixing the horrible comma-delimited mess with user,description,password), and more thorough code refactoring.

References

Advertisements

3 thoughts on “SCCM 1511 Command-Line Installation via PowerShell

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s