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.