TheGeekery

The Usual Tech Ramblings

Set-DnsServerResourceRecord and OldInputObject not found

While trying to help a co-worker today, I stumbled across a documentation error on Microsoft’s TechNet in relation to Set-DnsServerResourceRecord. The example uses multiple variable initialization, which unfortunately ends up making pointers.

Here is what we get from get-help.

PS C:\> get-help -examples set-dnsserverresourcerecord

NAME
    Set-DnsServerResourceRecord

SYNOPSIS
    Changes a resource record in a DNS zone.

    Example 1: Change the settings of a resource record

    PS C:\> $NewObj = $OldObj = Get-DnsServerResourceRecord -Name "Host01" -ZoneName "contoso.com" -RRType "A"
    PS C:\> $NewObj.TimeToLive = [System.TimeSpan]::FromHours(2)
    PS C:\> Set-DnsServerResourceRecord -NewInputObject $NewObj -OldInputObject $OldObj -ZoneName "contoso.com"
    -PassThru
    HostName                  RecordType Timestamp            TimeToLive      RecordData

    --------                  ---------- ---------            ----------      ----------

    Host01                       A          0                    02:00:00        2.2.2.2


    In this example, the time to live (TTL) value of the resource record named Host01 in the zone named contoso.com is
    changed to 2 hours.

    The first command assigns a resource record named Host01 in the zone named contoso.com to the variables $NewObj
    and $OldObj.

    The second command sets the TTL time span for $NewObj to 2 hours.

    The third command changes the properties of $OldObj to the settings specified for $NewObj in the previous command.

Okay, so the example seems pretty simple. They use the variable pass through to assign the return of Get-DnsServerResourceRecord to 2 variables at the same time. This should save some time, and avoid executing the same command twice. However, this actually causes an issue in this case, and here’s why.

PS C:\> $newobj = $oldobj = Get-DnsServerResourceRecord -ZoneName 'myzone.com' -name 'jatest' -RRType 'A'
PS C:\> $newobj

HostName                  RecordType Timestamp            TimeToLive      RecordData
--------                  ---------- ---------            ----------      ----------
jatest                    A          0                    01:00:00        2.2.2.2

PS C:\> $oldobj

HostName                  RecordType Timestamp            TimeToLive      RecordData
--------                  ---------- ---------            ----------      ----------
jatest                    A          0                    01:00:00        2.2.2.2

PS C:\> $newObj.RecordData.IPv4Address = [ipaddress]'8.8.8.8'
PS C:\> $newObj

HostName                  RecordType Timestamp            TimeToLive      RecordData
--------                  ---------- ---------            ----------      ----------
jatest                    A          0                    01:00:00        8.8.8.8

PS C:\> $oldObj

HostName                  RecordType Timestamp            TimeToLive      RecordData
--------                  ---------- ---------            ----------      ----------
jatest                    A          0                    01:00:00        8.8.8.8

What I’ve done here is grab the ‘jatest’ host record, outputted the two values, then updated the $newobj to have a new IP address of 8.8.8.8. However, as you can see here, there is an issue, the $oldObj value also updated the IP address. This has happened because $newObj is a pointer to $oldObj, meaning changes to one will apply to the other. Why does this matter? Well, the Set-DnsServerResourceRecord uses the old record information to find the record to update, and then updates it. This is important to understand because you could potentially have multiple IP records for ‘A’ records, or multiple NS records, or multiple MX records, etc. This means the data you are using to find the update must match the record you want to update, otherwise you could update a lot of records incorrectly. If it doesn’t, this happens instead:

PS C:\> Set-DnsServerResourceRecord -ZoneName 'myzone.com' -OldInputObject $oldObj -NewInputObject $newObj
Set-DnsServerResourceRecord : Resource record in OldInputObject not found in myzone.com zone on DNS01 server.
At line:1 char:1
+ Set-DnsServerResourceRecord -ZoneName 'myzone.com' -OldInputObject $oldObj -Ne ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (DNS01:root/Microsoft/...rResourceRecord) [Set-DnsServerResourceRec
   ord], CimException
    + FullyQualifiedErrorId : WIN32 9714,Set-DnsServerResourceRecord

The solution is either to call Get-DnsServerResourceRecord twice, or use the clone() function that is part of the returned operation. Such as this:

PS C:\> $oldobj = Get-DnsServerResourceRecord -ZoneName 'myzone.com' -name 'jatest' -RRType 'A'
PS C:\> $newObj = $oldObj.Clone()
PS C:\> $newObj.RecordData.IPv4Address = [ipaddress]'8.8.8.8'
PS C:\> $newObj

HostName                  RecordType Timestamp            TimeToLive      RecordData
--------                  ---------- ---------            ----------      ----------
jatest                    A          0                    01:00:00        8.8.8.8


PS C:\> $oldObj

HostName                  RecordType Timestamp            TimeToLive      RecordData
--------                  ---------- ---------            ----------      ----------
jatest                    A          0                    01:00:00        2.2.2.2

With this done, you can now use the Set-DnsServerResourceRecord with the right old and new values, and it will work successfully.

Edit: This is an old blog post, but it still stands true, if using PowerShell 5. However, in PowerShell 7 some things have changed. There is no Clone() operation available on [ciminstance], so you will get a failure with the error:

Method invocation failed because [Microsoft.Management.Infrastructure.CimInstance] does not contain a method named 'Clone'.

This obviously complicates updating the DNS records. There’s a couple of ways that this can be handled. You can use a generic function to serialize and deserialize the object into a new variable, which looks something like this:

function Clone-Object {
    param($InputObject)
    [System.Management.Automation.PSSerializer]::Deserialize(
        [System.Management.Automation.PSSerializer]::Serialize( $InputObject )
    )
}

This would then be used as such:

PS C:\> $oldObj = Get-DnsServerResourceRecord -ZoneName 'myzone.com' -name 'jatest' -RRType 'A'
PS C:\> $newObj = Clone-Object $oldObj

The alternative, and specific for this operation (though applicable for many objects) is to call ::new and pass in the old object, so the code would look something like this instead:

PS C:\> $oldObj = Get-DnsServerResourceRecord -ZoneName 'myzone.com' -name 'jatest' -RRType 'A'
PS C:\> $newObj = [CIMInstance]::new($oldObj)

I updated this post as I saw it pop up as a question on Reddit and figured I’d update in case somebody stumbles on the post again. Thanks to the posters there, and on Stack Overflow for the updated solutions.

Comments