Created
July 30, 2014 20:55
-
-
Save liveaverage/f61799a360039990087b to your computer and use it in GitHub Desktop.
Powershell wrapper for netsh and dnscmd functions used for DHCP scope creation.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
############################ | |
#AUTHOR: JR Morgan | |
#CREATED: 20120417 | |
#MODIFIED: 20140611 | |
############################ | |
<# | |
.Synopsis | |
Adds DHCP Scope to ALL specified DHCP servers. If split-scope is desired | |
the script uses IP Math to automatically add the desired exlcude ranges. | |
.Description | |
Creates a DHCP based on user-provided parameters. For 50/50 split-scope config, | |
the ordering of the DHCP Servers determines the upper/lower designation. The first | |
DHCP server specified will host upper scope portion, while the second DHCP server will | |
host the lower scope portion. Upper/Lower 'tags' are added to the description when using | |
the Split50 switch. | |
.Parameter DhcpServer | |
An array of DHCP Server IP names or addresses that will host the new DHCP scopes | |
.Parameter IPScope | |
The desired IP Scope | |
.Parameter IPMask | |
The desired IP mask for the scope | |
.Parameter Description | |
A brief scope description. Upper/Lower tags will | |
automatically be added if using the Split50 switch | |
.Parameter Gateway | |
The IP address of the router or gateway for this scope (Option 3) | |
.Parameter Dns | |
An array of DNS server IP addresses utilized by scope clients (Option 6). | |
.Parameter Domain | |
The fully-qualified domain name for scope clients (Option 15) | |
.Parameter StartAddress | |
An optional parameter used to specify the start address for distribution. | |
If no address is provided, the default StartAddress is the Network address +2 | |
.Parameter EndAddress | |
An optional parameter used to specify the end address for distribution. | |
If no address is provided, the default EndAddress is the Broadcast address -1 | |
.Parameter LeaseTime | |
Integer value (in seconds) for the desired address lease times associated | |
with the scope. Default: 691200 | |
.Parameter State | |
Set the DHCP scope state to enable (1) or disable (0). Default: 0 | |
.Parameter ExcludeAll | |
Excludes all addresses from distribution (Reservation required for IP distribution) | |
.Parameter Split50 | |
Configure a 50/50 split-scope between specified DHCP servers. | |
Ony two (2) DHCP servers may be specified. Exclusions are automatically | |
calculated for each server. | |
.Parameter NoRevLookup | |
A switched parameter used to disable the automatic creation of a corresponding reverse lookup zone. | |
DNS settings are retrieved using the 'Domain' parameter. If the reverse lookup zone already exists then | |
creation is skipped. | |
.Parameter NoDynamicDNS | |
A switched parameter used to disable dynamic DNS updates for a specific scope. | |
.Parameter OptionTftp | |
Adds an array of TFTP servers using DHCP option 150 | |
.Parameter Invoke | |
A switched parameter used to automatically invoke the netsh commands generated | |
for a new scope. Use cautiously! | |
.Notes | |
This script assumes you're running this as a domain admin or at least have DHCP Administrators group membership. | |
This script also assumes you would be using administrative credentials to actually invoke the generated | |
netsh commands. Command invocation will not work if you don't have the necessary privileges. | |
.Example | |
Set-Dhcp-Scope.ps1 -DhcpServer grudcpr03,grudcpr04 -IPScope 172.20.229.0 -IPMask 255.255.255.0 -Description "Description" -Gateway 172.20.229.1 -Dns 172.20.227.219,172.20.242.119 -Domain domain.com -StartAddress 172.20.229.2 -Split50 -State 1 | |
#> | |
#Parameters and required validation. Alias definitions for shorthand. Positional parameter options work, too. | |
Param | |
( | |
[Parameter(ValueFromPipeline = $True, Mandatory = $True)] | |
[ValidateCount(1,2)] | |
[Alias("dhcp")] | |
[String[]] | |
$DhcpServer | |
, | |
[Parameter(ValueFromPipeline = $True, Mandatory = $True)] | |
[ValidatePattern('^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$')] | |
[Alias("s")] | |
[String] | |
$IPScope | |
, | |
[Parameter(ValueFromPipeline = $True, Mandatory = $True)] | |
[ValidatePattern('^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$')] | |
[Alias("m")] | |
[String] | |
$IPMask | |
, | |
[Parameter(ValueFromPipeline = $True, Mandatory = $True)] | |
[Alias("desc")] | |
[String] | |
$Description = "" | |
, | |
[Parameter(ValueFromPipeline = $True, Mandatory = $True)] | |
[ValidatePattern('^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$')] | |
[Alias("g")] | |
[String] | |
$Gateway | |
, | |
[Parameter(ValueFromPipeline = $True, Mandatory = $True)] | |
[ValidatePattern('^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$')] | |
[Alias("n")] | |
[String[]] | |
$Dns | |
, | |
[Parameter(ValueFromPipeline = $True, Mandatory = $True)] | |
[Alias("d")] | |
[String] | |
$Domain = "gruadmin.gru.com" | |
, | |
[Parameter(ValueFromPipeline = $True)] | |
[ValidatePattern('^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$')] | |
[Alias("start")] | |
[String] | |
$StartAddress | |
, | |
[Parameter(ValueFromPipeline = $True)] | |
[ValidatePattern('^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$')] | |
[Alias("end")] | |
[String] | |
$EndAddress | |
, | |
[Parameter(ValueFromPipeline = $True, Mandatory = $False)] | |
[Alias("l")] | |
[Int] | |
$LeaseTime = 691200 | |
, | |
[Parameter(ValueFromPipeline = $True, Mandatory = $False)] | |
[ValidateRange(0,1)] | |
[Int] | |
$State = 0 | |
, | |
[Parameter(ValueFromPipeline = $True)] | |
[Alias("Exclude")] | |
[Switch] | |
$ExcludeAll | |
, | |
[Parameter(ValueFromPipeline = $True)] | |
[Alias("50")] | |
[Switch] | |
$Split50 | |
, | |
[Parameter(ValueFromPipeline = $True)] | |
[Switch] | |
$NoRevLookup | |
, | |
[Parameter(ValueFromPipeline = $True)] | |
[Switch] | |
$NoDynamicDNS | |
, | |
[Parameter(ValueFromPipeline = $True)] | |
[ValidatePattern('^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$')] | |
[String[]] | |
$OptionTftp | |
, | |
[Parameter(ValueFromPipeline = $False)] | |
[Alias("x")] | |
[Switch] | |
$Invoke | |
) | |
#region IPMath | |
Function ConvertTo-BinaryIP { | |
<# | |
.Synopsis | |
Converts a Decimal IP address into a binary format. | |
.Description | |
ConvertTo-BinaryIP uses System.Convert to switch between decimal and binary format. The output from this function is dotted binary. | |
.Parameter IPAddress | |
An IP Address to convert. | |
#> | |
[CmdLetBinding()] | |
Param( | |
[Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)] | |
[Net.IPAddress]$IPAddress | |
) | |
Process { | |
Return [String]::Join('.', $( $IPAddress.GetAddressBytes() | | |
ForEach-Object { [Convert]::ToString($_, 2).PadLeft(8, '0') } )) | |
} | |
} | |
Function ConvertTo-DecimalIP { | |
<# | |
.Synopsis | |
Converts a Decimal IP address into a 32-bit unsigned integer. | |
.Description | |
ConvertTo-DecimalIP takes a decimal IP, uses a shift-like operation on each octet and returns a single UInt32 value. | |
.Parameter IPAddress | |
An IP Address to convert. | |
#> | |
[CmdLetBinding()] | |
Param( | |
[Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)] | |
[Net.IPAddress]$IPAddress | |
) | |
Process { | |
$i = 3; $DecimalIP = 0; | |
$IPAddress.GetAddressBytes() | ForEach-Object { $DecimalIP += $_ * [Math]::Pow(256, $i); $i-- } | |
Return $DecimalIP | |
} | |
} | |
Function ConvertTo-DottedDecimalIP { | |
<# | |
.Synopsis | |
Returns a dotted decimal IP address from either an unsigned 32-bit integer or a dotted binary string. | |
.Description | |
ConvertTo-DottedDecimalIP uses a regular expression match on the input string to convert to an IP address. | |
.Parameter IPAddress | |
A string representation of an IP address from either UInt32 or dotted binary. | |
#> | |
[CmdLetBinding()] | |
Param( | |
[Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)] | |
[String]$IPAddress | |
) | |
Process { | |
Switch -RegEx ($IPAddress) { | |
"([01]{8}\.){3}[01]{8}" { | |
Return [String]::Join('.', $( $IPAddress.Split('.') | ForEach-Object { [Convert]::ToUInt32($_, 2) } )) | |
} | |
"\d" { | |
$IPAddress = [UInt32]$IPAddress | |
$DottedIP = $( For ($i = 3; $i -gt -1; $i--) { | |
$Remainder = $IPAddress % [Math]::Pow(256, $i) | |
($IPAddress - $Remainder) / [Math]::Pow(256, $i) | |
$IPAddress = $Remainder | |
} ) | |
Return [String]::Join('.', $DottedIP) | |
} | |
default { | |
Write-Error "Cannot convert this format" | |
} | |
} | |
} | |
} | |
Function ConvertTo-MaskLength { | |
<# | |
.Synopsis | |
Returns the length of a subnet mask. | |
.Description | |
ConvertTo-MaskLength accepts any IPv4 address as input, however the output value | |
only makes sense when using a subnet mask. | |
.Parameter SubnetMask | |
A subnet mask to convert into length | |
#> | |
[CmdLetBinding()] | |
Param( | |
[Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)] | |
[Alias("Mask")] | |
[Net.IPAddress]$SubnetMask | |
) | |
Process { | |
$Bits = "$( $SubnetMask.GetAddressBytes() | ForEach-Object { [Convert]::ToString($_, 2) } )" -Replace '[\s0]' | |
Return $Bits.Length | |
} | |
} | |
Function ConvertTo-Mask { | |
<# | |
.Synopsis | |
Returns a dotted decimal subnet mask from a mask length. | |
.Description | |
ConvertTo-Mask returns a subnet mask in dotted decimal format from an integer value ranging | |
between 0 and 32. ConvertTo-Mask first creates a binary string from the length, converts | |
that to an unsigned 32-bit integer then calls ConvertTo-DottedDecimalIP to complete the operation. | |
.Parameter MaskLength | |
The number of bits which must be masked. | |
#> | |
[CmdLetBinding()] | |
Param( | |
[Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)] | |
[Alias("Length")] | |
[ValidateRange(0, 32)] | |
$MaskLength | |
) | |
Process { | |
Return ConvertTo-DottedDecimalIP ([Convert]::ToUInt32($(("1" * $MaskLength).PadRight(32, "0")), 2)) | |
} | |
} | |
Function Get-NetworkAddress { | |
<# | |
.Synopsis | |
Takes an IP address and subnet mask then calculates the network address for the range. | |
.Description | |
Get-NetworkAddress returns the network address for a subnet by performing a bitwise AND | |
operation against the decimal forms of the IP address and subnet mask. Get-NetworkAddress | |
expects both the IP address and subnet mask in dotted decimal format. | |
.Parameter IPAddress | |
Any IP address within the network range. | |
.Parameter SubnetMask | |
The subnet mask for the network. | |
#> | |
[CmdLetBinding()] | |
Param( | |
[Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)] | |
[Net.IPAddress]$IPAddress, | |
[Parameter(Mandatory = $True, Position = 1)] | |
[Alias("Mask")] | |
[Net.IPAddress]$SubnetMask | |
) | |
Process { | |
Return ConvertTo-DottedDecimalIP ((ConvertTo-DecimalIP $IPAddress) -BAnd (ConvertTo-DecimalIP $SubnetMask)) | |
} | |
} | |
Function Get-BroadcastAddress { | |
<# | |
.Synopsis | |
Takes an IP address and subnet mask then calculates the broadcast address for the range. | |
.Description | |
Get-BroadcastAddress returns the broadcast address for a subnet by performing a bitwise AND | |
operation against the decimal forms of the IP address and inverted subnet mask. | |
Get-BroadcastAddress expects both the IP address and subnet mask in dotted decimal format. | |
.Parameter IPAddress | |
Any IP address within the network range. | |
.Parameter SubnetMask | |
The subnet mask for the network. | |
#> | |
[CmdLetBinding()] | |
Param( | |
[Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)] | |
[Net.IPAddress]$IPAddress, | |
[Parameter(Mandatory = $True, Position = 1)] | |
[Alias("Mask")] | |
[Net.IPAddress]$SubnetMask | |
) | |
Process { | |
Return ConvertTo-DottedDecimalIP $((ConvertTo-DecimalIP $IPAddress) -BOr ` | |
((-BNot (ConvertTo-DecimalIP $SubnetMask)) -BAnd [UInt32]::MaxValue)) | |
} | |
} | |
Function Get-NetworkSummary ( [String]$IP, [String]$Mask ) { | |
If ($IP.Contains("/")) | |
{ | |
$Temp = $IP.Split("/") | |
$IP = $Temp[0] | |
$Mask = $Temp[1] | |
} | |
If (!$Mask.Contains(".")) | |
{ | |
$Mask = ConvertTo-Mask $Mask | |
} | |
$DecimalIP = ConvertTo-DecimalIP $IP | |
$DecimalMask = ConvertTo-DecimalIP $Mask | |
$Network = $DecimalIP -BAnd $DecimalMask | |
$Broadcast = $DecimalIP -BOr | |
((-BNot $DecimalMask) -BAnd [UInt32]::MaxValue) | |
$NetworkAddress = ConvertTo-DottedDecimalIP $Network | |
$RangeStart = ConvertTo-DottedDecimalIP ($Network + 1) | |
$RangeEnd = ConvertTo-DottedDecimalIP ($Broadcast - 1) | |
$ScopeStart = ConvertTo-DottedDecimalIP ($Network + 2) | |
$ScopeEnd = ConvertTo-DottedDecimalIP ($Broadcast + 1) | |
$BroadcastAddress = ConvertTo-DottedDecimalIP $Broadcast | |
$MaskLength = ConvertTo-MaskLength $Mask | |
$BinaryIP = ConvertTo-BinaryIP $IP; $Private = $False | |
Switch -RegEx ($BinaryIP) | |
{ | |
"^1111" { $Class = "E"; $SubnetBitMap = "1111" } | |
"^1110" { $Class = "D"; $SubnetBitMap = "1110" } | |
"^110" { | |
$Class = "C" | |
If ($BinaryIP -Match "^11000000.10101000") { $Private = $True } } | |
"^10" { | |
$Class = "B" | |
If ($BinaryIP -Match "^10101100.0001") { $Private = $True } } | |
"^0" { | |
$Class = "A" | |
If ($BinaryIP -Match "^00001010") { $Private = $True } } | |
} | |
$NetInfo = New-Object Object | |
Add-Member NoteProperty "Network" -Input $NetInfo -Value $NetworkAddress | |
Add-Member NoteProperty "Broadcast" -Input $NetInfo -Value $BroadcastAddress | |
Add-Member NoteProperty "Range" -Input $NetInfo ` | |
-Value "$RangeStart - $RangeEnd" | |
Add-Member NoteProperty "Mask" -Input $NetInfo -Value $Mask | |
Add-Member NoteProperty "RangeStart" -Input $NetInfo -Value $RangeStart | |
Add-Member NoteProperty "RangeEnd" -Input $NetInfo -Value $RangeEnd | |
Add-Member NoteProperty "ScopeStart" -Input $NetInfo -Value $ScopeStart | |
Add-Member NoteProperty "ScopeEnd" -Input $NetInfo -Value $ScopeEnd | |
Add-Member NoteProperty "MaskLength" -Input $NetInfo -Value $MaskLength | |
Add-Member NoteProperty "Hosts" -Input $NetInfo ` | |
-Value $($Broadcast - $Network - 1) | |
Add-Member NoteProperty "Class" -Input $NetInfo -Value $Class | |
Add-Member NoteProperty "IsPrivate" -Input $NetInfo -Value $Private | |
Return $NetInfo | |
} | |
#endregion | |
$c = $null | |
$DecimalIP = ConvertTo-DecimalIP $IPScope | |
$DecimalMask = ConvertTo-DecimalIP $IPMask | |
#Basic Calculations in the event of missing Start/End: | |
$Network = $DecimalIP -BAnd $DecimalMask | |
$Broadcast = $DecimalIP -BOr | |
((-BNot $DecimalMask) -BAnd [UInt32]::MaxValue) | |
$NetworkAddress = ConvertTo-DottedDecimalIP $Network | |
$RangeStart = ConvertTo-DottedDecimalIP ($Network + 2) | |
$RangeEnd = ConvertTo-DottedDecimalIP ($Broadcast - 1) | |
foreach ($s in [array]$DhcpServer) | |
{ | |
$Desc = $null | |
#Autogenerate upper/lower descriptions: | |
if ($DhcpServer.count -gt 1 -and $Description -match "vlan" -and $Split50) | |
{ | |
if ($s -eq $DhcpServer[0]) { $Desc = ($Description -replace "vlan","[upper] VLAN") } | |
if ($s -eq $DhcpServer[1]) { $Desc = ($Description -replace "vlan","[lower] VLAN") } | |
} | |
elseif ($DhcpServer.count -gt 1 -and $Split50) | |
{ | |
if ($s -eq $DhcpServer[0]) { $Desc = $Description + " [upper]" } | |
if ($s -eq $DhcpServer[1]) { $Desc = $Description + " [lower]" } | |
} | |
else | |
{ | |
$Desc = $Description | |
} | |
$c += "netsh dhcp server \\$s add scope $IPScope $IPMask `"$Desc`"`r`n" | |
$c += "netsh dhcp server \\$s scope $IPScope set state $State`r`n" | |
if ([string]::IsNullOrEmpty($StartAddress)) | |
{ | |
$StartAddress = $RangeStart | |
} | |
if ([string]::IsNullOrEmpty($EndAddress)) | |
{ | |
$EndAddress = $RangeEnd | |
} | |
$c += "netsh dhcp server \\$s scope $IPScope add iprange $StartAddress $EndAddress`r`n" | |
if (($Split50 -and $DhcpServer.count -gt 1) -or ($ExcludeAll -and $DhcpServer.count -gt 1)) | |
{ | |
#Scope Start decimal: | |
$ssd = ConvertTo-DecimalIP $StartAddress | |
#Scope End decimal: | |
$sed = ConvertTo-DecimalIP $EndAddress | |
#Math bytes: | |
$th = [int64]$sed - [int64]$ssd | |
#Halve the scope total hosts#: | |
$hh = $th/2 | |
#Subtract from Scope End Decimal: | |
#Lower Scope End (this is the end of the lower scope range): | |
$lsedd = ConvertTo-DottedDecimalIP ($sed-$hh) | |
#Upper Scope Start (this is beginning of the upper scope range): | |
$ussdd = ConvertTo-DottedDecimalIP (($ssd+$hh)+1) | |
#Config Upper scope (exclusions for lower scope) | |
if ($s -eq $DhcpServer[0]) | |
{ | |
if (-not $ExcludeAll) | |
{ | |
$c += "netsh dhcp server \\$s scope $IPScope add excluderange $StartAddress $lsedd`r`n" | |
} | |
else | |
{ | |
$c += "netsh dhcp server \\$s scope $IPScope add excluderange $StartAddress $EndAddress`r`n" | |
} | |
} | |
#Config Lower scope (exclusions for upper scope) | |
if ($s -eq $DhcpServer[1]) | |
{ | |
if (-not $ExcludeAll) | |
{ | |
$c += "netsh dhcp server \\$s scope $IPScope add excluderange $ussdd $EndAddress`r`n" | |
} | |
else | |
{ | |
$c += "netsh dhcp server \\$s scope $IPScope add excluderange $StartAddress $EndAddress`r`n" | |
} | |
} | |
} | |
#Lease Time: | |
$c += "netsh dhcp server \\$s scope $IPScope set optionvalue 51 DWORD `"$LeaseTime`"`r`n" | |
#Gateway: | |
$c += "netsh dhcp server \\$s scope $IPScope set optionvalue 3 IPADDRESS `"$Gateway`"`r`n" | |
#DNS Domain Name: | |
$c += "netsh dhcp server \\$s scope $IPScope set optionvalue 15 STRING `"$Domain`"`r`n" | |
#DNS Servers: | |
$dnslist = $null | |
foreach ($d in [array]$Dns) | |
{ | |
$dnslist += "`"$d`" " | |
} | |
$c += "netsh dhcp server \\$s scope $IPScope set optionvalue 6 IPADDRESS $dnslist`r`n" | |
$c += "`r`n" | |
#TFTP Servers: | |
$tftplist = $null | |
if(([array]$OptionTftp).count -gt 0) | |
{ | |
foreach ($ts in [array]$OptionTftp) | |
{ | |
$tftplist += "`"$ts`" " | |
} | |
$c += "netsh dhcp server \\$s scope $IPScope set optionvalue 150 IPADDRESS $tftplist`r`n" | |
$c += "`r`n" | |
} | |
if ($NoDynamicDNS) | |
{ | |
$c += "netsh dhcp server \\$s scope $IPScope set DnsConfig 0 0 0 0" | |
} | |
} | |
if (-not $NoRevLookup) | |
{ | |
$revname = $IPScope.Split(".") | |
$zones = (Invoke-Expression -Command "dnscmd $Domain /enumzones") | %{$_ -split "\s+"} | ?{ ` | |
$_.StartsWith("$($revname[2]).$($revname[1]).$($revname[0]).in-addr.arpa") ` | |
-or ` | |
$_.StartsWith("$($revname[0]).$($revname[1]).$($revname[2]).in-addr.arpa")} | |
if (([string[]]$zones).count -lt 1) | |
{ | |
$c += "dnscmd $Domain /zoneadd $($revname[2]).$($revname[1]).$($revname[0]).in-addr.arpa /dsprimary" | |
$c += "dnscmd $Domain /Config $($revname[2]).$($revname[1]).$($revname[0]).in-addr.arpa /AllowUpdate 2" | |
$c += "dnscmd $Domain /Config $($revname[2]).$($revname[1]).$($revname[0]).in-addr.arpa /Aging 1" | |
} | |
else | |
{ | |
Write-Output "Skipping Reverse Lookup Zone creation. Detected pre-existing zone(s): `r`n $zones `r`n" | |
} | |
} | |
Write-Output "Generated the following netsh commands:`r`n" | |
Write-Output $c | |
if ($Invoke) | |
{ | |
Write-Host "Received Invoke parameter; executing all commands.`r`n" | |
#Write-Host "These are the commands that were generated from your input:`n$($c)`n" | |
#$execute = Read-Host "Definitely execute these commands? (USE CARE)? (Y/N)" | |
#Double-check execution/invocation of netsh | |
#if ($execute -match "Y") | |
#{ | |
Invoke-Expression $c | |
#} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment