Managing Hostnames vs. GUIDs on a Pull Server

I’m a bit perplexed that the output of the ‘Configuration’ keyword in Powershell is a MOF named after the hostname of a node, while a Pull server expects/demands a MOF named after a GUID.

The DSC Book covers how to do this (manually renaming a hostname.MOF to guid.MOF before putting it on the pull server), but I’m wondering if you guys have a strategy for managing this disparity.

After I finish setting up the https Pull Server and DSC Configuration files, I want to put them in source control so my teammates can edit them as needed. Right now, here is the process for updating a DSC script:
[ol]
[li]Check out the DSC scripts, and make appropriate change.[/li]
[li]Run the Configuration keyword to generate a new Hostname.MOF file for each of the nodes[/li]
[li]Log into the Pull server, browse to the location where the MOFs are stored.[/li]
[li]Look into each MOF and build a table of Hostname->GUIDs (this can be saved for later)[/li]
[li]Rename each Hostname.MOF to its correct Guid.MOF[/li]
[li]Copy each Guid.MOF to the Pull Server[/li]
[li](The next subsequent run of each node will pick up the new MOF and enact the changes)[/li]
[/ol]

That block in the middle where one needs to manually translate a Hostname.MOF to Guid.MOF is what I’m concerned about. Is there a better strategy for managing this? Should i be generating new GUIDs every time my DSC scripts are changed, and just re-configure the nodes’ respective LCMs to use the new GUIDs? If I do that, should I be deleting the old Guid.MOFs in order to keep things clean?

I’d love to hear how you guys are solving this problem!

The purpose of a GUID is so that multiple computers can be told to query the same config, and because GUIDs are - in theory - an easier-to-manage and more-unique identifier than a host name.

What you’re experiencing right now is the lack of any management tooling to help us keep track. I’m sure that tooling is on the way, but it might be something named “System Center,” not “built into Windows.” So for your step 4 right now, you’re going to have to come up with something on your own. That might be dropping the GUID into an extra field in AD, or using a SQL Server database, Kinda up to you.

You do NOT generate a new GUID each time you re-run a DSC config. Remember, the LCM on each node needs to be configured with a GUID - that GUID should more or less remain the same. If a machine’s config needs to change, you generate a new MOF having the same GUID, so that the machine is pulling the new MOF automatically.

But yes, right now, the “how do I keep track of which host is using which GUID” is very much a missing link.

Hey Don, Thanks for the response. That’s kinda expected, I was just hoping for some people to chime in with how they’ve tackled this problem. :slight_smile:

I should clarify what I mean by the “new GUID” thing. Once option I can take is to write a ‘DeployNodeConfigsToPullServer.ps1’ script. That script would:
[ol]
[li]Delete all existing MOFs from the Pull server[/li]
[li]Foreach Node being processed:[/li]
[li]Generate a whole new GUID[/li]
[li]Rename the Hostname.MOF to the new Guid.MOF[/li]
[li]Copy the new Guid.MOF to the Pull server[/li]
[li]Configure the node’s LCM to pull the new Guid[/li]
[/ol]

It seems a bit wasteful and heavy handed, but it would ensure that the MOFs on the Pull server are up to date and nothing stale is left behind. I guess the only reason I’m considering this approach is because I can’t think of an elegant way to maintain a Hostname->GUID table (which could get cumbersome when we add new nodes).

So I answered this, at least so far, by using Active Directory. Once of DSC’s features is not requiring Active Directory, but I think for those with it they can leverage it.

For one off configs that are specific to a single machine, I just use the machines Active Directory GUID, and typically have something in the script used to create the MOF file, that ties these together:

([guid]([adsisearcher]"(samaccountname=$ComputerName`$)").FindOne().Properties["objectguid"][0]).Guid

For farms of machines sharing a single GUID, I was working with either making a group or account, both of which have GUIDs, and just tying the machines to that in some manner. I’m still not sure if I like an Active Directory account with the name, or if I like using a group, and then even putting the machines in that group, which is then searchable through the powershell script creating the MOFs. Another option I thought of was using a field already within the Active Directory account, or computer description field. As you can tell I have not 100% decided on the option I want to use yet, as I am still working with my team on what makes the most sense for us.

I’m doing my best to avoid creating something like a CSV file, spreadsheet, database, or just any other “thing” that I have to reference to get the information I need, instead trying to make it all quickly and easily available within my powershell console.

That’s brilliant. All of our machines are in AD so I can just pull the GUID from there instead!

I agree with you, I was resisting pretty hard trying to come up with some other moving part that contained a Hostname->GUID mapper. Since AD already does that, I’ll just leverage that!

On a side note, renaming files isn’t always a necessary step. You can set up your configurations so that the node name is a GUID (either hard-coded, or passed in via parameters when the MOF is compiled; whatever your preference), and it’ll produce MOF files that are already named properly. If I were building a configuration script with the intention of assigning the same GUID to a whole group of computers, I’d probably have the GUID hard-coded in the configuration script, as the default value for a parameter.

I agree, typically the MOF shouldn’t change name (GUID). To me that means you built a different configuration. So it is less of a MOF to Computer relationship, as it is MOF to Configuration File, and which computers are pulling that file. Which is why I ended up with things like DSC-ApplicationName-ServerType groups, and put the machines in that AD group, and use it’s AD GUID, for the MOF file GUID, so I had an easy relationship between them.

Yes, I think the point of multi-nodes to one MOF, and that the MOF does not need to change GUID at every creation (but that the checksum has to be updated), are important points for dealing getting DSC working in larger environments.

I see what you guys are saying about sharing a MOF with multiple machines, but in this particular application, each machine will have a separate MOF (and thus, each needs its own unique GUID).

My solution is to build a script that “translates” the Hostname.MOF to Guid.MOF using the Active Directory GUID, and then push the Guid.MOF to the Pull server. That way, whenever someone on my team subsequently edits the DSC script, they just run this deployment which will take care of pushing the new version of the MOF to the pull server.

That works. I’d still lean toward generating the MOF files with the proper name rather than renaming them later, but that’s personal preference. Somewhere along the line, similar tooling and lookups are going to happen either way.

But if i generate MOF files based on the GUID, they can no longer be used in push mode, right? (Not that I really expect people to use Push mode when I have a Pull server set up, but I’d like to leave that option open).

Correct; MOF files have to have GUID names when used in a Pull server, and hostnames as the file name when used for Push. Unless you feel like compiling two MOFs automatically for every node, at some point, you’d have to rename something to use the opposite mode. Keep in mind, though, that as soon as you push a configuration to a server, you’ve changed its mode from Pull to Push. You’d have to go back and fix the LCM configuration later to get it to start pulling from the server again.

Generating the MOF file based off a GUID name has nothing to do with the mode the machine is in. That would change only if you pushed a configuration to the machine.

On some of my scripts, at the bottom it looks like this:

Configuration DSC_File_Server
{
	.. .
}
Configuration SetPullMode
{
	.. .
}

DSC_File_Server -ComputerName $ComputerName
$Guid = ([guid]([adsisearcher]"(samaccountname=$ComputerName`$)").FindOne().Properties["objectguid"][0]).Guid
$Destination = "\\PULLSERVER\C`$\Program Files\WindowsPowerShell\DscService\Configuration\$Guid.mof"
Copy-Item -Path .\DSC_File_Server\$ComputerName.mof -Destination $Destination
New-DSCCheckSum -ConfigurationPath $Destination -Force
Start-DscConfiguration .\DSC_File_Server -Verbose -Wait -Force
SetPullMode -ComputerName $ComputerName -Guid $Guid -OutputPath .\SetPullMode
Set-DscLocalConfigurationManager -ComputerName $ComputerName .\SetPullMode -Verbose

So I still end up with a folder that has the computer name in it, and the ComputerName.MOF file, but then on the pull server it is a GUID. I suppose I could just change the name, and then copy it over. In this particular script, I ran the config first, and then changed it to pull mode after just because I like to see the first run of some of them. Hopefully that is helpful in some way.

I’ve been playing around a little with this issue, trying to keep track of both what configurations are tied to what GUIDs, and then which machines are configured to pull those GUIDs. I opted for a SQL database approach, and came up with a couple of scripts:

Function Publish-DSCConfiguration{
    Param (
        [Parameter(Mandatory=$True)]
        [ValidateScript({test-path $_ })]
        [System.IO.FileInfo]$Path,

        [String]$Description,

        [Parameter(Mandatory=$True)]
        [Guid]$Guid
    )
    Write-Verbose "Validating parameters"
    If (!($Path.name -like "*.mof")){
        Write-Verbose "Specified path not a MOF file.  Searching directory"
        $path = Get-ChildItem $path -filter *.mof
        if ($path.count -eq 0){
            Write-error "MOF file not found in specified Path" -ErrorAction Stop
        }
        Elseif ($path.count -gt 1){
            Write-error "Multiple MOF files found in specified Path.  Please specify correct file" -ErrorAction Stop
        }
        else {
            Write-Verbose "MOF file found at $path"
        }
    }
    $dest = "\\PULLSERVER\c`$\Program Files\WindowsPowerShell\DscService\Configuration\$guid.mof"
    if(!(Test-Path -path (split-path -Path $dest -Parent))){
        Write-Error "Cannot access destination directory $(split-path -path $dest.fullname -parent)" -erroraction Stop
    }
    Write-Verbose "Attempt database connection before updating files"
    Try{
        $SQLReadConn = New-Object System.Data.SqlClient.SqlConnection
        $SQLReadConn.ConnectionString = "server=.\SQLExpress;database=DSCData;trusted_connection=true;"
        $SQLReadConn.Open()
        $SQLWriteConn = New-Object System.Data.SqlClient.SqlConnection
        $SQLWriteConn.ConnectionString = "server=.\SQLExpress;database=DSCData;trusted_connection=true;"
        $SQLWriteConn.Open()
    } Catch{
        Write-Error "Unable to connect to database, halting script." -ErrorAction Stop
    }
    If (test-path $dest -ea SilentlyContinue){
        Write-Verbose "File exists, creating backup"
        Copy $dest "$(split-path $dest -parent)\Backup\$(Split-path $dest -Leaf)-$(get-date -UFormat "%m.%d.%y-%H.%M")" -Force -ErrorAction Stop
    }
    copy $Path $dest -Force -ErrorAction Stop
    New-DscCheckSum $dest -force -ErrorAction Stop
    #Once publishing is complete, write data to tracking database
    #If new config, write Configname ($path.name), GUID, creation date
    #If updated config, update/write configname ($Path.name), GUID, modify date
    $ConfigName = $((split-path $path -parent).split('\')[-1])
    $SQLReadCmd = New-Object System.Data.SqlClient.SqlCommand
    $SQLReadCmd.Connection = $SQLReadConn
    $SQLReadCmd.CommandText = "SELECT * FROM ConfigList WHERE Guid='$guid'"
    $result = $SQLReadCmd.ExecuteReader()

    If ($result.length -eq $Null){
        Write-Verbose "GUID not found in database, writing entry as new configuration"
        $SQLString = "INSERT INTO ConfigList (Guid,ConfigName,CreationDate,Description) VALUES('{0}','{1}','{2}','{3}')" -f $Guid,$ConfigName,$(get-date),$Description
    } Else {
        Write-Verbose "Entry found in database for GUID, updating record"
        $SQLString = "UPDATE ConfigList SET ConfigName = '$ConfigName', UpdateTime = '$(Get-date)', Description = '$Description' WHERE Guid = '$guid'"
    }
    $SQLWriteCmd = New-Object System.Data.SqlClient.SqlCommand
    $SQLWriteCmd.Connection = $SQLWriteConn
    $SQLWriteCMD.CommandText = $SQLString
    $SQLWriteCmd.executenonquery() | Out-Null
    $SQLReadConn.Close()
    $SQLWriteConn.Close()
}

I’ll generate my mof like normal, then use this script to move it and rename it with a guid, then write the info into a sql database, recording GUID, creation date/time, last updated date/time, and the name of the configuration before it was moved. So for a new config the command might be this:

Publish-DSCConfiguration -Path C:\dsc\BaselineServer\localhost.mof -Guid $([GUID]::NewGuid()) -Verbose

Then once it is out on the pull server, I’ll use this code to tell a server to pull that file:

Configuration SetPullMode{
    Param(
        [parameter(Mandatory=$True)]
        [string]$guid
    )
    LocalConfigurationManager{
        ConfigurationMode = "ApplyAndAutoCorrect"
        ConfigurationID=$guid
        RefreshMode='Pull'
        DownloadManagerName='WebDownloadManager'
        DownloadManagerCustomData=@{
            ServerUrl = 'https://PSDSCPullServerCert:8080/PSDSCPullServer.svc';
        }
    }
}

Function Set-DSCPullConfig{
    Param(
        [Parameter(Mandatory=$True,
                    ValueFromPipeline=$True,
                    ValueFromPipelineByPropertyName=$True)]
        [Alias('Computername','Computer')]
        [String[]]$NodeName,

        [Parameter(Mandatory=$True)]
        [Guid]$Guid
    )
    Begin{
        write-verbose "Generating MOF"
        $MofPath = SPlit-path -path $(SetPullMode -guid $guid -OutputPath C:\DSC\SetPullMode) -Parent
        Write-Verbose "Testing database access"
        Try{
            $SQLReadConn = New-Object System.Data.SqlClient.SqlConnection
            $SQLReadConn.ConnectionString = "server=.\SQLExpress;database=DSCData;trusted_connection=true;"
            $SQLReadConn.Open()
            $SQLReadConn.Close()
            $SQLWriteConn = New-Object System.Data.SqlClient.SqlConnection
            $SQLWriteConn.ConnectionString = "server=.\SQLExpress;database=DSCData;trusted_connection=true;"
            $SQLWriteConn.Open()
            $SQLWriteConn.Close()
        } Catch{
            Write-Error "Unable to connect to database, halting script." -ErrorAction Stop
        }

    }
    Process{
        Foreach ($Computer in $NodeName){
            Write-Verbose "Pushing LCM config to $computer"
            Copy-Item "$mofpath\localhost.meta.mof" "$mofpath\$computer.meta.mof"
            Set-DscLocalConfigurationManager -ComputerName $Computer -path $MofPath -ErrorAction Stop
            Write-Verbose "Updating configuration database"
            $SQLReadConn = New-Object System.Data.SqlClient.SqlConnection
            $SQLReadConn.ConnectionString = "server=.\SQLExpress;database=DSCData;trusted_connection=true;"
            $SQLReadConn.Open()
            $SQLWriteConn = New-Object System.Data.SqlClient.SqlConnection
            $SQLWriteConn.ConnectionString = "server=.\SQLExpress;database=DSCData;trusted_connection=true;"
            $SQLWriteConn.Open()
            $SQLReadCmd = New-Object System.Data.SqlClient.SqlCommand
            $SQLReadCmd.Connection = $SQLReadConn
            $SQLReadCmd.CommandText = "SELECT * FROM AssignList WHERE ComputerName='$Computer'"
            $result = $SQLReadCmd.ExecuteReader()

            If ($result.length -eq $Null){
                Write-Verbose "Computer $computer not found in database, writing entry as new configuration"
                $SQLString = "INSERT INTO AssignList (Guid,ComputerName,AssignDate) VALUES('{0}','{1}','{2}')" -f $Guid,$Computer,$(get-date)
            } Else {
                Write-Verbose "Entry found in database for $computer, updating record"
                $SQLString = "UPDATE AssignList SET Guid = '$guid', AssignDate = '$(Get-date)' WHERE ComputerName = '$Computer'"
            }
            $SQLWriteCmd = New-Object System.Data.SqlClient.SqlCommand
            $SQLWriteCmd.Connection = $SQLWriteConn
            $SQLWriteCMD.CommandText = $SQLString
            $SQLWriteCmd.executenonquery() | Out-Null
            Write-Verbose "Closing database connections"
            $SQLReadConn.Close()
            $SQLWriteConn.Close()

        }
        #After setting the configuration update a database table
        #include computername, assigned guid, date the config was pushed
    }
    End{
    }
}

I just parameterized the GUID portion of the LCM configuration so I can pass it whatever, then I generate the MOF, push it out to the server (a push to tell it to pull, still mixes me up a bit) then I write an entry in a second table that lists the server name, the assigned guid and the date it was set.

Not super elegant or anthing, and I still need to clean up the code a bit, document, etc. but so far it seems to work.

Since all of my nodes need to have a unique GUID (they don’t share MOF files), I ended up using Raymond’s trick of grabbing the GUID from active directory.

First, I have a script to generate the regular Hostname.MOF files. People can use these in Push mode to test their changes if they want to (they have to use -Force). Then, I have the following script to translate the Hostname.MOF to Guid.MOF and deploy that to the Pull server:

#Load up the configuration data
. ..\ConfigurationData.ps1

# Copy all the MOF files based on Hostname into one based on their AD GUID (these get
# sent to the Pull server, which only works on GUIDs, not hostnames).
$source_path = $ConfigurationData.SourcePath #a local path containing the Hostname.MOFs
$dest_path = $ConfigurationData.DestPath #a UNC path on the Pull server containing the Guid.MOFs
if (!(Test-Path -path $dest_path)) {New-Item $dest_path -Type Directory}
$ConfigurationData.AllNodes | % {
    $node = $_.NodeName
    $guid = ([guid]([adsisearcher]"(samaccountname=$node`$)").FindOne().Properties["objectguid"][0]).Guid
    $source = $source_path + $node + ".mof"
    $dest = $dest_path + $guid + ".mof"
    Copy-Item $source $dest -Force
    New-DSCCheckSum $dest -Force
}

Then, when configuring PullMode on the target nodes, I use the same snippet of code to pull the GUID from active directory, which ensures they always run the proper MOF.

The approach I’m taking is to query the server’s LCM directly when I run the DSC configuration script to generate a MOF.
I’m setting a parameter that expects a computer name when the configuration script is run and then it queries the server(s) defined to retrieve the GUID from LCM. It then uses that as the node name and generates a MOF file with the GUID as the file name. Saves a lot of hassle trying track GUID’s and writing a ton of tools.

Is anyone having multiple servers pull the same GUID? Wouldn’t doing so make it impossible to configure things like hostname and IP address with DSC? I’m just trying to figure out if there are any disadvantages to using the AD GUID in an environment with AD.

Clint Armstrong wrote:Is anyone having multiple servers pull the same GUID? Wouldn't doing so make it impossible to configure things like hostname and IP address with DSC? I'm just trying to figure out if there are any disadvantages to using the AD GUID in an environment with AD.

Correct; if you need to set node-specific data like IPs or hostnames, you either can’t use DSC for that, or you need to have unique GUIDs (and separate MOF documents) for each node. I think the technique of assigning the same GUID to multiple computers is probably not going to see a lot of use, when it’s fairly easy to get all the benefits of that approach (without the disadvantages) by using ConfigurationData and a more dynamic configuration function to process it.

Could you not add some extra node checking within the MOF or script? Something that does infact look at hostname or AD guid and decide assign IP from there?

Personally I hate this whole GUID/ one MOF nonsense. I wish I could just assign multiple MOF’s to one computer and make generic MOF’s for everything. That way I could just assign what I want to computers quickly and have something like this.

Server 1 :
DomainComputer-MOF
BasicConfig-MOF
Production-MOF
WebClusterA-MOF

Server 2 :
LoadBalance-MOF
PerformanceConfig-MOF
WebClusterB-MOF

Server 3 :
PerformanceConfig-MOF
SQLClusterA-MOF
DevNetwork-MOF

That’s more or less what composite configurations are intended to provide (but all eventually compiled into one MOF document, by the time it’s sent down to the servers.) You’d have a top-level configuration script something like this:

configuration Whatever
{
    Import-DscResource -ModuleName MyCompanyCompositeResourceModule
    
    Node Server1
    {
        DomainComputer domainComputerSettings
        {
            Param1 = 'Value'
            Param2 = 'Value'
        }

        BasicConfig basicConfigSettings
        {
            Param1 = 'Value'
        }
        
        # etc
    }
}

And BasicConfig, DomainComputer, etc, would themselves be configurations that you’ve saved as composite resources in a module called (in this example) “MyCompanyCompositeResourceModule.” I assume you’d find a better name for it than the crap that I make up. :wink:

In practice, you’d probably have a single configuration script with logic built around your ConfigurationData hashtable, instead of hard-coding which settings go to each node like this, but you get the idea.

John Totten wrote:Could you not add some extra node checking within the MOF or script?

The MOF files are pretty static things. They tell the target computer which resources to execute, with which parameters, and in what order according to dependencies you’ve defined. I don’t think there’s any way to send an identical MOF file to two different computers and have them produce different results, unless you’ve written a custom resource which behaves weirdly that way. (And even then, it would have to get its node-specific information from somewhere other than the MOF file, which isn’t really a good practice for DSC.)