ad-joining/register-computer/join.ps1 (307 lines of code) (raw):

# # Copyright 2019 Google LLC # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # #-------------------------------------------------------------------------------------------- # Include this scriptlet as specialize script (sysprep-specialize-script-ps1) # to have VM instances automatically join the domain. # # iex((New-Object System.Net.WebClient).DownloadString('https://%domain%/')) #-------------------------------------------------------------------------------------------- $ErrorActionPreference = "Stop" $InformationPreference = "Continue"; <# .SYNOPSIS Retrieves the diagnotics bucket from metadata or an empty string if attribute has not been set. .OUTPUTS [string] .EXAMPLE $bucket = Get-DiagnosticsBucket #> function Get-DiagnosticsBucket { process { $DiagnosticsBucket = [string]::Empty; try { # Get instance level metadata attribute $DiagnosticsBucket = (Invoke-RestMethod -Headers $MetadataHeaders ` -Uri "$($MetadataUri)/instance/attributes/adjoin-diagnostics-bucket"); } catch { # Swallow exception that may ocurr when metadata attribute is not set } if([string]::IsNullOrEmpty($DiagnosticsBucket)) { try { # Get project level metadata attribute $DiagnosticsBucket = (Invoke-RestMethod -Headers $MetadataHeaders ` -Uri "$($MetadataUri)/project/attributes/adjoin-diagnostics-bucket"); } catch { # Swallow exception that may ocurr when metadata attribute is not set } } return $DiagnosticsBucket; } } <# .SYNOPSIS Determines the version of PktMon .OUTPUTS Version of PktMon or $Null if PktMon is not available .EXAMPLE Get-PktMonVersion #> function Get-PktMonVersion { process { $Command = Get-Command -Name "PktMon.exe" -ErrorAction SilentlyContinue; if($Null -ne $Command) { return $Command.Version; } return $Null; } } <# .SYNOPSIS Starts diagnostics depending on whether a bucket was configure or not .PARAMETER DiagnosticsBucket String denoting the GCS bucket the diagnostics should be copied to .EXAMPLE Start-JoinDiagnotics -DiagnosticsBucket "adjoin-test"; #> function Start-JoinDiagnostics { param ( [string] $DiagnosticsBucket ); process { if(-not [string]::IsNullOrEmpty($DiagnosticsBucket)) { Write-Information -MessageData "AD Join diagnostics: Enabled"; $DiagnosticsCaptureFile = "${env:TEMP}\capture.etl"; $Version = Get-PktMonVersion; if($Null -ne $Version) { if($Version -ge (New-Object Version 10, 0, 17763, 1879)) { # Use PktMon for tracing & pktmon start -c --pkt-size 0 -f $DiagnosticsCaptureFile | Out-Null; } else { # Fall back to NetEventPacketCapture if only an older version of PktMon is available New-NetEventSession -Name "adjoin" -LocalFilePath $DiagnosticsCaptureFile | Out-Null; Add-NetEventPacketCaptureProvider -SessionName "adjoin" -TruncationLength 0 | Out-Null; foreach($Adapter in (Get-NetAdapter)) { Add-NetEventNetworkAdapter -Name "$($Adapter.Name)" | Out-Null; } Start-NetEventSession -Name "adjoin"; } } else { # PktMon does not exist in this version of Windows or does not provide trace recording, falling back to netsh trace & netsh trace start capture=yes perfMerge=no report=disabled tracefile=$DiagnosticsCaptureFile | Out-Null; } } else { Write-Information -Message "AD Join diagnostics: Not enabled"; } } } <# .SYNOPSIS Stops diagnostics depending on whether a bucket was configure or not .PARAMETER DiagnosticsBucket String denoting the GCS bucket the diagnostics should be copied to .EXAMPLE Stop-JoinDiagnotics -DiagnosticsBucket "adjoin-test"; #> function Stop-JoinDiagnostics { param ( [string] $DiagnosticsBucket ); process { if(-not [string]::IsNullOrEmpty($DiagnosticsBucket)) { $DiagnosticsCaptureFile = "${env:TEMP}\capture.etl"; $DiagnosticsOutputFile = $DiagnosticsCaptureFile; $Timestamp = [DateTime]::Now.ToUniversalTime().ToString("yyyy-MM-dd-HH-mm"); $Version = Get-PktMonVersion; if($Null -ne $Version) { if($Version -ge (New-Object Version 10, 0, 17763, 1879)) { # Stop PktMon trace and convert to pcapng $DiagnosticsOutputFile = "$env:SystemRoot\temp\capture.pcapng"; & pktmon stop | Out-Null; & pktmon pcapng $DiagnosticsCaptureFile -o $DiagnosticsOutputFile | Out-Null; } else { # Stop NetEventPacketCapture trace Stop-NetEventSession -Name "adjoin"; Remove-NetEventSession -Name "adjoin"; } } else { # Stop netsh trace & netsh trace stop | Out-Null; } $Extension = [System.IO.Path]::GetExtension($DiagnosticsOutputFile); $DiagnosticsBucketFile = "gs://$DiagnosticsBucket/captures/$($JoinInfo.ComputerName)-$Timestamp$Extension"; & gsutil -q cp $DiagnosticsOutputFile $DiagnosticsBucketFile; Write-Information -MessageData "AD Join diagnostics: Packet capture copied to $DiagnosticsBucketFile"; } } } $MetadataUri = "http://metadata.google.internal/computeMetadata/v1"; $MetadataHeaders = @{"Metadata-Flavor" = "Google"}; # Retrieve diagnostics bucket from metadata $DiagnosticsBucket = Get-DiagnosticsBucket; # Diagnostics started depending on bucket setting Start-JoinDiagnostics -DiagnosticsBucket $DiagnosticsBucket; # Fetch IdToken that we can use to authenticate the instance with. $IdToken = (Invoke-RestMethod ` -Headers $MetadataHeaders ` -Uri "$($MetadataUri)/instance/service-accounts/default/identity?audience=%scheme%:%2F%2F%domain%%2F&format=full") # Determine AD site $AdSite = ""; try { # Retrieve domain name from adjoin service $Domain = Invoke-RestMethod ` -Headers @{"Authorization" = "Bearer $IdToken"} ` -Method GET ` -Uri "%scheme%://%domain%/domain"; $Result = & nltest /dsgetdc:$Domain /DS_6 /TRY_NEXT_CLOSEST_SITE; $PatternMatches = ($Result | Select-String -Pattern "^Our Site Name: (.+)$").Matches; if($Null -ne $PatternMatches) { $AdSite = $PatternMatches.Groups[1].Value; } } catch { # Swallow exception and continue without site-awareness } if([string]::IsNullOrEmpty($AdSite)) { Write-Host "AD site-awareness: Failed to determine closest AD site"; } else { Write-Host "AD site-awareness: Using site '$AdSite' for join"; } # Register computer in Active Directory. $JoinInfo = try { Invoke-RestMethod ` -Headers @{"Authorization" = "Bearer $IdToken"} ` -Method POST ` -Uri "%scheme%://%domain%/?ad_site=$AdSite" } catch { $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream()) $reader.BaseStream.Position = 0 $reader.DiscardBufferedData() $errorData = $reader.ReadToEnd(); # Load Balancer may return something different than JSON try { $errorObject = $errorData | ConvertFrom-Json; Write-Host $_.Exception.Message Write-Host "Error is:" $errorObject.error } catch { Write-Error $errorData; } Write-Host "Failed to register computer account." } if ($JoinInfo) { Write-Host "Successfully registered computer account." $JoinInfoRedacted = $JoinInfo.PSObject.copy() $JoinInfoRedacted.ComputerPassword = "*" $JoinInfoRedacted | Format-List $NewComputerName = $JoinInfo.ComputerName $OriginalComputerName = $JoinInfo.OriginalComputerName if ($NewComputerName -ne $OriginalComputerName) { Write-Host "Renaming computer from $OriginalComputerName to $NewComputerName" Rename-Computer -ComputerName localhost -NewName $NewComputerName -Force -PassThru -Verbose } if ($NewComputerName.Length -gt 15) { Write-Host "WARNING: Computer name exceeds NetBIOS limits - domain join might fail" } # Join the computer using the computer account that has just been registered. # Because the join is performed using a known computer password (the one generated # by the API), it is called an "unsecure join". # # If there are multiple domain controllers in the domain, the computer account # might not have been replicated to all domain controllers yet. To avoid any # race condition, use the same domain controller for the join as was used # to create the computer account. $Credentials = (New-Object pscredential -ArgumentList ([pscustomobject]@{ ` UserName = $Null Password = (ConvertTo-SecureString -String $JoinInfo.ComputerPassword -AsPlainText -Force)[0]})) $Attempt = 0 do { try { Add-Computer ` -ComputerName localhost ` -Server $JoinInfo.DomainController ` -DomainName $JoinInfo.Domain ` -Credential $Credentials ` -OUPath $JoinInfo.OrgUnitPath ` -Options UnsecuredJoin,PasswordPass,JoinWithNewName ` -Force Write-Host "Computer successfully joined to domain" Stop-JoinDiagnostics -DiagnosticsBucket $DiagnosticsBucket; break } catch { # Authentication occasionally fails after the computer account's password # has been reset. In this case, retry. $Attempt++ if ($Attempt -lt 20) { Write-Host "Joining computer failed, retry pending: $($_.Exception.Message)" Start-Sleep -Seconds 3 } else { Stop-JoinDiagnostics -DiagnosticsBucket $DiagnosticsBucket; throw [System.ArgumentException]::new( "Joining computer to domain failed: $($_.Exception.Message)") } } } while ($True) }