Automate Exchange installation and configuration with DSC Part 3:Automate creation and publishing to pull server

This is the third post of a serie around DSC and my personal journey:

Part 1: Pull server (setup and querying node status)

Part 2: Configure node’s LCM in a bulk operation reading GUID from AD and using individual certificates

Part 3: Automate creation and publishing to pull server of configurations reading Configuration and Environment Data files

In this post I will cover how to create MOF files reading a Configuration,  Environment data and one CSV file.

In general you create a Configuration and an Environment Data file. In the Environment Data file you will define dedicated properties for each node if necessary.

Here is an example, which contains 4 nodes, a DAG configuration and regional namespaces:

@{
    AllNodes = @(
        #Settings under 'NodeName = *' apply to all nodes.
        @{
            NodeName        = '*'
            ProductKey      = 'HWY43-FY882-FM8YD-GR2XV-QH6DA'
            DefaultOAB      = 'Default Offline Address Book'
            PagefileSize    = '32778'
            #PSDscAllowPlainTextPassword = $true
            #The paths to the CSV files generated by the Server Role Requirements Calculator
            ServersCsvPath               = "C:\DSC\CSVs\Servers.csv"
            MailboxDatabasesCsvPath      = "C:\DSC\CSVs\MailboxDatabases.csv"
            MailboxDatabaseCopiesCsvPath = "C:\DSC\CSVs\MailboxDatabaseCopies.csv"
            
        }
        @{
            Nodename        = 'fabex01'
            FQDN            = 'fabex01.fabrikam.local'
            Location        = 'emea'
            DAGID           = 'dag01'
            Role            = 'FirstDAGMember'
            Mode            = 'operation'
            CertificateFile = 'C:\DSC\Certs\fabex01.cer'
            Thumbprint      = '2BD755C186CAB212B90ADAA48A3F27BA06473BBD'
        }
        @{
            Nodename        = 'fabex02'
            FQDN            = 'fabex02.fabrikam.local'
            Location        = 'emea'
            DAGID           = 'dag01'
            Role            = 'FirstDAGMember'
            Mode            = 'operation'
            CertificateFile = 'C:\DSC\Certs\fabex02.cer'
            Thumbprint      = '0720CA37DF4A7CD82EF0719D7670E1857D6D6AA5'
        }
        @{
            Nodename        = 'fabex03'
            FQDN            = 'fabex03.fabrikam.local'
            Location        = 'amer'
            DAGID           = 'dag01'
            Role            = 'FirstDAGMember'
            Mode            = 'operation'
            CertificateFile = 'C:\DSC\Certs\fabex03.cer'
            Thumbprint      = '8740BD236C35E4168DBE70469A8FF1203DBC55A4'
        }
        @{
            Nodename        = 'fabex04'
            FQDN            = 'fabex04.fabrikam.local'
            Location        = 'amer'
            DAGID           = 'dag01'
            Role            = 'FirstDAGMember'
            Mode            = 'operation'
            CertificateFile = 'C:\DSC\Certs\fabex04.cer'
            Thumbprint      = 'FA266475142CBCBF30764D435C5C7E72D899EE24'
        }
    );
    #Settings that are unique per DAG will go in separate hash table entries.
    DAG01 = @(
        @{
            ###DAG Settings###
            DAGName                              = 'DAG01'           
            AlternateWitnessDirectory            = $null
            AlternateWitnessServer               = $null
            AutoDagAutoReseedEnabled             = $true
            AutoDagDatabaseCopiesPerDatabase     = '4'
            AutoDagDatabaseCopiesPerVolume       = '4'
            AutoDagDatabasesRootFolderPath       = 'C:\ExchangeDatabases'
            AutoDagDiskReclaimerEnabled          = $true
            AutoDagTotalNumberOfDatabases        = '16'
            AutoDagTotalNumberOfServers          =  '4'
            AutoDagVolumesRootFolderPath         = 'C:\ExchangeVolumes'
            DatabaseAvailabilityGroupIpAddresses = '255.255.255.255'
            DatacenterActivationMode             = 'DagOnly'
            #DomainController                     = $null
            ManualDagNetworkConfiguration        = $false
            NetworkCompression                   = 'Enabled'
            NetworkEncryption                    = 'Enabled'
            ReplayLagManagerEnabled              = $true
            ReplicationPort                      = '64327'
            WitnessDirectory                     = 'C:\FSW'
            WitnessServer                        = 'fabsrv01.fabrikam.local'
            SkipDagValidation                    = $True
            DbNameReplacements                   = @{"nn" = "01"}
            #database settings
            AllowServiceRestart                  = $false
            AutoDagExcludeFromMonitoring         = $false
            BackgroundDatabaseMaintenance        = ''
            CalendarLoggingQuota                 = '6GB'
            CircularLoggingEnabled               = $true
            DatabaseCopyCount                    = '4'
            DataMoveReplicationConstraint        = 'none'
            DeletedItemRetention                 = '30.00:00:00'
            DomainController                     = $null
            EventHistoryRetentionPeriod          = '7.00:00:00'
            IndexEnabled                         = $true
            #IsExcludedFromProvisioning           = $false
            IssueWarningQuota                    = '1700MB'
            #IsSuspendedFromProvisioning          = $false
            #JournalRecipient                     = $null
            MailboxRetention                     = '30.00:00:00'
            MountAtStartup                       = $true
            ProhibitSendQuota                    = '2GB' 
            ProhibitSendReceiveQuota             = '2.25GB'
            RecoverableItemsQuota                = '6GB'
            RecoverableItemsWarningQuota         =  '6227702784'
            RetainDeletedItemsUntilBackup        = $false
            #AdServerSettingsPreferredServer      = $null
            SkipInitialDatabaseMount             = $false
        }
    );
    #CAS settings that are unique per site will go in separate hash table entries as well.
    EMEA = @(
        @{
            InternalNamespace            = 'mail.fabrikam.local'
            ExternalNamespace            = 'mail.fabrikam.com'
            #ClientAccessServer Settings
            AutoDiscoverSiteScope      = 'HQ-Site'
        }
    );
    AMER = @(
        @{
            InternalNamespace            = 'mailamer.fabrikam.local'
            ExternalNamespace            = 'mailamer.fabrikam.com'
            #ClientAccessServer Settings
            AutoDiscoverSiteScope      = 'HQ-Site'
        }
    );
}

As you can see those nodes have different settings. Assuming now you have not only 4 servers; maybe 40 or 80. That means you have to add for each node a block.

This causes the Environmetal Data file to grow and could be somehow error-prone. Especially when you cannot use the same certificate on all nodes in order to secure your MOF files.

Solution

As a solution I created a script, which imports the Configuration and Environmental Data files. It’s not needed anymore to have for your nodes a block defined. But you will need to provide an additional CSV file, which has at least a column called ServerName ( as in my previous script to configure the LCM described here).

The script will read the specified CSV file and will convert the values into a hashtable and add this one to the array AllNodes in the Environmental Data. The CSV file for the example above looks like this:

ServerName,FQDN,Location,DAGID,Role,Mode
fabex01,fabex01.fabrikam.local,emea,dag01,FirstDAGMember,operation
fabex02,fabex02.fabrikam.local,emea,dag01,AdditionalDAGMember,operation
fabex03,fabex03.fabrikam.local,amer,dag01,AdditionalDAGMember,operation
fabex04,fabex04.fabrikam.local,amer,dag01,AdditionalDAGMember,operation

Now you will ask me: “Where is the column for the NodeName, CertificateFile and Thumbprint?”

Well, that’s part of the script. The script won’t change anything of your files. An additional task is to read the CSV file, retrieve and export the certificate from the nodes and finally add the complete node data to the Environmental Data.

But this is not the only advantage. The script will also publish the created files to the directory of your pull server and creates a new checksum in order to let the node know that there is a new config waiting for it. It uses the objectguid of the computer account in AD as GUID. Thus means you don’t have to worry about keeping track of GUIDs to corresponding nodes.

This simplifies and automates the process of creating and publishing MOF files dramatically.

How does it looks like?

As a best practice I created a folder structure like this:New-DSCConfig_01

Under DSC there are several folders:

  • Certs: Contains the exported certificates from the nodes
  • Config: Contains the created MOF files, before they get copied to the pull server
  • CSVs: Contains your self-created CSV file like in the example above and the output from the Exchange 2013 Server Role Requirements Calculator
  • Scripts: Contains all your scripts and your Configuration and Environmental Data files

I run the script with the following parameters to create and publish the MOF files:

.\New-DSCConfigsFromFiles.ps1 -ServerCSVFile C:\DSC\CSVs\LabServers.csv -DSCConfig C:\DSC\Scripts\DemoConfig.ps1 -DSCConfigSettings C:\DSC\Scripts\DemoConfig-Config.psd1 -MOFsPath C:\DSC\Config -CertPath C:\DSC\Certs -PublishToPull -SkipPing -Verbose

New-DSCConfig_02

When you use the -Verbose switch the script will tell you more info about what was found and what it is currently working on. As an example:

The script found a certificate with the thumbprint 2BD755C186CAB212B90ADAA48A3F27BA06473BBD on node fabex01 and exported it to C:\DSC\Certs\fabex01.cer. Then it used the GUID 9516aede-a6dd-4d63-ac18-a93fb1e48bd0, which was taken from the attribute objectguid from AD of this computer account, and copied the MOF file to the target folder C:\Program Files\WindowsPowerShell\DscService\Configuration.

When you look into the Certs folder you’ll find the exported certificates, which are used to encrypt the credentials:

New-DSCConfig_03

All created MOF files before they got published in the Config folder:

New-DSCConfig_04

And finally the published MOF files:

New-DSCConfig_05

New-DSCConfigsFromFiles

You can download the script here. The script accepts the following parameters:

Parameter

Description

ServerCSVFile Full path to the CSV file containing the nodes
DSCConfig Path to your DSC Configuration file
DSCConfigSettings Path to your DSC Environment Data file
MOFsPath Path to a folder to store the generated MOF files
CertPath Path to a folder where all the certificates will be stored
MOFsTargetPath Path to the folder on the pull server to publish the files. Default is ‘C:\Program Files\WindowsPowerShell\DscService\Configuration’
PublishToPull Switch to publish the files to the folder defined in MOFsTargetPath. Default is $true
SkipPing By default the script uses Test-Connection to check the availability of a node. If you use this switch this test will be bypassed. Default is $false
SkipCert Switch to bypass the certificate export. This is useful when you are using the same certificate on all nodes. Note: If you are going not to encrypt your credentials, make sure you have set the property PSDscAllowPlainTextPassword to $true!

This was the third part of my journey. I hope you find it useful and I could help you to simplify things.

14 thoughts on “Automate Exchange installation and configuration with DSC Part 3:Automate creation and publishing to pull server

  1. Pingback: Automate Exchange installation and configuration with DSC Part 2:Configure node’s LCM | The clueless guy

  2. Pingback: Automate Exchange installation and configuration with DSC Part 1:Pull server | The clueless guy

  3. Pingback: To run iPowershell Desired State Configuration(DSC):How to enforce a consistency check? | The clueless guy

  4. Pingback: Powershell DSC – Desired State Control – Resourses | DevOpsAut.com

  5. Pingback: What is uploadReadAheadSize? | The clueless guy

  6. Pingback: 文件配置DSC - Exchange中文站

  7. Have been playing around with DSC and these scripts. I can get the new-dscconfigsfromfiles.ps1 to work but only if I run the configuration script first to generate the .mof file. I don’t get an error when running the script but it doesn’t seem to generate the .mof on it’s own. Everything else works nicely:)

    Like

  8. Hi, i want to try your Script but i don’t know, what is the content of DemoConfig.ps1 and DemoConfig-Config.psd1.
    Can you give an example?

    Like

    • Hi Timo,
      sorry for the delay! Here is an example from my lab:
      Configuration DemoConfig
      {
      Import-DscResource -Module xExchange 1.26.0.0

      #settings for all nodes
      Node $AllNodes.NodeName
      {
      xExchClientAccessServer CAS
      {
      Identity = $Node.NodeName
      #AlternateServiceAccountCredential = $ASACreds
      Credential = $ShellCreds
      #CleanUpInvalidAlternateServiceAccountCredentials = $true
      #AutoDiscoverSiteScope = “Site1″,”Site2”
      AutoDiscoverServiceInternalUri = “https://autodiscover.fabrikam.local/autodiscover/autodiscover.xml”
      #RemoveAlternateServiceAccountCredentials = $true
      }

      xExchTransportService TransportService
      {
      Identity = $Node.NodeName
      Credential = $ShellCreds
      AllowServiceRestart = $true
      MaxPerDomainOutboundConnections = ’50’
      }

      xExchActiveSyncVirtualDirectory EASVdir
      {
      Identity = “$($Node.NodeName)\Microsoft-Server-ActiveSync (Default Web Site)”
      Credential = $ShellCreds
      Domaincontroller = “fabdc01.fabrikam.local”
      #ActiveSyncServer = “https://bla/Microsoft-Server-ActiveSync”
      BadItemReportingEnabled =$true
      BasicAuthEnabled = $true
      ClientCertAuth = “Ignore”
      CompressionEnabled = $true
      ExtendedProtectionFlags = ‘none’ #@(‘AllowDotlessSPN’) #@(‘none’)#@(“AllowDotlessSPN”,”NoServicenameCheck”)
      ExtendedProtectionSPNList = @(“http/mail.fabrikam.com”) #@(“http/mail.fabrikam.com”,”http/mail.fabrikam.local”)
      ExtendedProtectionTokenChecking = “Allow”
      ExternalAuthenticationMethods = @(“Basic”,”Kerberos”)
      ExternalUrl = “https://mail.fabrikam.com/Microsoft-Server-ActiveSync”
      InstallIsapiFilter = $true
      InternalAuthenticationMethods = @(“Basic”,”Kerberos”)
      InternalUrl = “https://mail.fabrikam.local/Microsoft-Server-ActiveSync”
      MobileClientCertificateAuthorityURL = “http://whatever.com/CA”
      MobileClientCertificateProvisioningEnabled = $false
      MobileClientCertTemplateName = “MyTemplateforEAS”
      #Name = “$($Node.NodeName) EAS Site”
      RemoteDocumentsActionForUnknownServers = “Block”
      RemoteDocumentsAllowedServers = @(“AllowedA”,”AllowedC”)
      RemoteDocumentsBlockedServers = @(“BlockedA”,”BlockedC”)
      RemoteDocumentsInternalDomainSuffixList = @(“InternalA”,”InternalB”)
      SendWatsonReport = $false
      WindowsAuthEnabled = $true
      }

      xExchAutodiscoverVirtualDirectory AutoD
      {
      Identity = “$($Node.NodeName)\Autodiscover (Default Web Site)”
      Credential = $ShellCreds
      Domaincontroller = “fabdc01.fabrikam.local”
      BasicAuthentication = $true
      DigestAuthentication = $true
      ExtendedProtectionFlags = @(“AllowDotlessSPN”,”NoServicenameCheck”)
      ExtendedProtectionSPNList = @(“http/mail.fabrikam.com”,”http/mail.fabrikam.local”,”http/wxweqc”)
      ExtendedProtectionTokenChecking = “Allow”
      OAuthAuthentication = $false
      WindowsAuthentication = $true
      WSSecurityAuthentication = $true
      }

      xExchWebServicesVirtualDirectory EWS
      {
      Identity = “$($Node.NodeName)\EWS (Default Web Site)”
      Credential = $ShellCreds
      #CertificateAuthentication = $true
      Domaincontroller = “fabdc01.fabrikam.local”
      BasicAuthentication = $true
      DigestAuthentication = $true
      ExtendedProtectionFlags = @(“AllowDotlessSPN”,”NoServicenameCheck”)
      ExtendedProtectionSPNList = @(“http/mail.fabrikam.com”,”http/mail.fabrikam.local”,”http/wxweqc”)
      ExtendedProtectionTokenChecking = “Allow”
      ExternalUrl = “https://mail.fabrikam.com/EWS/Exchange.asmx”
      GzipLevel = “High”
      InternalNLBBypassUrl = “https://$($Node.FQDN)/EWS/Exchange.asmx”
      InternalUrl = “https://mail.fabrikam.local/EWS/Exchange.asmx”
      OAuthAuthentication = $false
      WindowsAuthentication = $true
      WSSecurityAuthentication = $true
      }
      }
      }
      if ($ShellCreds -eq $null)
      {
      #$ShellCreds = Get-Credential -Message ‘Enter credentials for establishing Remote Powershell sessions to Exchange’
      $User = “Fabrikam\administrator”
      $PWord = ConvertTo-SecureString –String ‘Pa$$w0rd’ –AsPlainText -Force
      $ShellCreds = New-Object –TypeName System.Management.Automation.PSCredential –ArgumentList $User, $PWord

      }
      if ($ASACreds -eq $null)
      {
      #$ShellCreds = Get-Credential -Message ‘Enter credentials for establishing Remote Powershell sessions to Exchange’
      $UserASA = “ASA@fabrikam.com”
      $PWordASA = ConvertTo-SecureString –String ‘Pa$$w0rd!’ –AsPlainText -Force
      $ASACreds = New-Object –TypeName System.Management.Automation.PSCredential –ArgumentList $UserASA, $PWordASA

      }

      Like

Leave a comment