Private/Imports/Import-HydrationAutopilotProfile.ps1
|
function Import-HydrationAutopilotProfile { <# .SYNOPSIS Creates a Windows Autopilot deployment profile from a template. .DESCRIPTION Skips creation when any profile with a matching prefixed or legacy name exists. Retries with safe defaults when the tenant lacks licensing for pre-provisioning or hardware hash extraction. .PARAMETER Template Parsed enrollment template object. .PARAMETER ProfileName Prefixed profile display name. .PARAMETER TemplateBaseName Legacy unprefixed template name. .PARAMETER DeviceNameTemplate Optional device naming template override. .OUTPUTS Hydration result objects. #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [psobject]$Template, [Parameter(Mandatory)] [string]$ProfileName, [Parameter()] [string]$TemplateBaseName, [Parameter()] [string]$DeviceNameTemplate ) try { # Check if profile exists - check both prefixed and unprefixed names (backward compat with pre-prefix profiles) $safeProfileName = $ProfileName -replace "'", "''" $filter = "displayName eq '$safeProfileName'" if (-not [string]::IsNullOrWhiteSpace($TemplateBaseName) -and $TemplateBaseName -ne $ProfileName) { $safeOriginalName = $TemplateBaseName -replace "'", "''" $filter += " or displayName eq '$safeOriginalName'" } $existingProfiles = Invoke-MgGraphRequest -Method GET -Uri "beta/deviceManagement/windowsAutopilotDeploymentProfiles?`$filter=$filter" -ErrorAction Stop $existingMatch = Select-HydrationExistingMatch -Candidates $existingProfiles.value -Name $ProfileName -LegacyName $TemplateBaseName -PreferTagged $existingProfileId = $existingMatch.id if ($existingProfileId) { # Verify the profile still exists (handles eventual consistency after deletes) if (-not (Test-GraphResourceExists -Uri "beta/deviceManagement/windowsAutopilotDeploymentProfiles/$existingProfileId")) { Write-Verbose "Autopilot profile '$ProfileName' (ID: $existingProfileId) no longer exists - proceeding to create" $existingProfileId = $null } } if ($existingProfileId) { Write-HydrationLog -Message " Skipped: $ProfileName" -Level Info return New-HydrationResult -Name $ProfileName -Type 'AutopilotDeploymentProfile' -Id $existingProfileId -Action 'Skipped' -Status 'Already exists' } if (-not $PSCmdlet.ShouldProcess($ProfileName, "Create Autopilot deployment profile")) { Write-HydrationLog -Message " WouldCreate: $ProfileName" -Level Info return New-HydrationResult -Name $ProfileName -Type 'AutopilotDeploymentProfile' -Action 'WouldCreate' -Status 'DryRun' } # Autopilot deployment profiles reject the ' - ' separator in descriptions; # use a space-only separator so no dash is introduced by the hydration tag. $profileDescription = New-HydrationDescription -ExistingText $Template.description -Separator ' ' # Apply custom device name template if provided $deviceName = if ($DeviceNameTemplate) { $DeviceNameTemplate } else { $Template.deviceNameTemplate } # Build profile body matching the exact format the Graph API accepts # (verified against Intune console's working POST payload) $profileBody = @{ "@odata.type" = "#microsoft.graph.azureADWindowsAutopilotDeploymentProfile" displayName = $ProfileName description = $profileDescription deviceNameTemplate = $deviceName locale = $Template.locale preprovisioningAllowed = [bool]$Template.preprovisioningAllowed deviceType = $Template.deviceType hardwareHashExtractionEnabled = [bool]$Template.hardwareHashExtractionEnabled roleScopeTagIds = @() hybridAzureADJoinSkipConnectivityCheck = [bool]$Template.hybridAzureADJoinSkipConnectivityCheck outOfBoxExperienceSetting = @{ deviceUsageType = $Template.outOfBoxExperienceSetting.deviceUsageType escapeLinkHidden = [bool]$Template.outOfBoxExperienceSetting.escapeLinkHidden privacySettingsHidden = [bool]$Template.outOfBoxExperienceSetting.privacySettingsHidden eulaHidden = [bool]$Template.outOfBoxExperienceSetting.eulaHidden userType = $Template.outOfBoxExperienceSetting.userType keyboardSelectionPageSkipped = [bool]$Template.outOfBoxExperienceSetting.keyboardSelectionPageSkipped } } # Serialize to JSON to avoid Invoke-MgGraphRequest internal serialization issues $profileJson = $profileBody | ConvertTo-Json -Depth 10 -Compress Write-Verbose "Autopilot profile body: $profileJson" $newProfile = $null $firstError = $null try { $newProfile = Invoke-MgGraphRequest -Method POST -Uri "beta/deviceManagement/windowsAutopilotDeploymentProfiles" -Body $profileJson -ContentType 'application/json' -ErrorAction Stop } catch { $firstError = $_ $retryStatusCode = $null if ($_.Exception.Response.StatusCode) { $retryStatusCode = [int]$_.Exception.Response.StatusCode } $strategyErrMsg = Get-GraphErrorMessage -ErrorRecord $_ Write-Verbose "Autopilot profile '$ProfileName' failed: $strategyErrMsg" if ($retryStatusCode -eq 400) { # Retry with safe defaults for properties that require specific licensing $profileBody.preprovisioningAllowed = $false $profileBody.hardwareHashExtractionEnabled = $false $retryJson = $profileBody | ConvertTo-Json -Depth 10 -Compress Write-Verbose "Autopilot profile retry body (safe defaults): $retryJson" try { $newProfile = Invoke-MgGraphRequest -Method POST -Uri "beta/deviceManagement/windowsAutopilotDeploymentProfiles" -Body $retryJson -ContentType 'application/json' -ErrorAction Stop $firstError = $null Write-Verbose "Autopilot profile '$ProfileName' succeeded with safe defaults" } catch { Write-Verbose "Autopilot profile '$ProfileName' also failed on retry: $(Get-GraphErrorMessage -ErrorRecord $_)" } } } if ($firstError) { throw $firstError } Write-HydrationLog -Message " Created: $ProfileName" -Level Info return New-HydrationResult -Name $ProfileName -Type 'AutopilotDeploymentProfile' -Id $newProfile.id -Action 'Created' -Status 'Success' } catch { $errMessage = Get-GraphErrorMessage -ErrorRecord $_ Write-HydrationLog -Message " Failed: $ProfileName - $errMessage" -Level Warning return New-HydrationResult -Name $ProfileName -Type 'AutopilotDeploymentProfile' -Action 'Failed' -Status $errMessage } } |