diff --git a/README.md b/README.md index d88dd88..03b49e9 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,45 @@ Both features are **gated** — if a status stays `Pending` for more than a few ./scripts/register-providers.ps1 -SubscriptionId ``` +### Run the all-in-one quickstart on macOS + +Install PowerShell 7, Azure CLI, and Python 3, then run the same cross-platform script from a `pwsh` terminal: + +```bash +brew install --cask powershell +brew install azure-cli python + +az login +pwsh -File ./scripts/quickstart.ps1 \ + -SubscriptionId \ + -ResourceGroup +``` + +The script accepts the same parameters on Windows and macOS. It creates an isolated Python virtual environment and runs the demo with Azure AD authentication; no API key is required. + +### Required Azure access + +The simplest option is to give the deploying customer **Owner** on the target subscription for the deployment window. Owner covers resource-group creation, provider/feature registration, resource deployment, and the role assignment created by this template. Remove or reduce that access after validation. + +For a least-privilege customer deployment, split the work: + +1. A subscription administrator registers `Microsoft.Storage`, `Microsoft.CognitiveServices`, and the gated `Microsoft.CognitiveServices/OpenAI.ContextCacheAllowed` feature, and creates the target resource group. +2. At the target **resource-group scope**, grant the customer **Contributor** plus **Role Based Access Control Administrator**. +3. The customer runs `quickstart.ps1 -SkipPrerequisiteRegistration` against that existing resource group. This avoids subscription-level checks that a resource-group-scoped identity may not be allowed to read. + +The two resource-group roles are both required by the current template: + +| Role | Why it is needed | +|---|---| +| **Contributor** | Creates the Azure OpenAI account/deployment, Context Cache account/container, and ARM deployment record. | +| **Role Based Access Control Administrator** | Creates the `Cognitive Services OpenAI User` role assignment used by the keyless demo. `Contributor` explicitly cannot create role assignments. | + +Provider registration requires the provider's `/register/action` permission at subscription scope; built-in **Contributor** and **Owner** include it. Preview-feature registration is also subscription-scoped and may additionally require Microsoft allow-list approval. Email **azurecontextcacherp@microsoft.com** if `OpenAI.ContextCacheAllowed` remains `Pending`. + +After deployment, a person or workload calling the model with Microsoft Entra ID needs **Cognitive Services OpenAI User** on the Azure OpenAI account. The template grants it automatically to the deploying user when it creates a new account; the quickstart attempts the same grant when reusing an existing account. + +RBAC alone does not guarantee deployment success. Confirm that the subscription has Azure OpenAI S0/model capacity for the selected region and is approved for the Context Cache preview before the customer session. + --- ## What gets created diff --git a/scripts/quickstart.ps1 b/scripts/quickstart.ps1 index aaf548b..dc4bef8 100644 --- a/scripts/quickstart.ps1 +++ b/scripts/quickstart.ps1 @@ -41,6 +41,10 @@ .PARAMETER SkipPython Skip the auto venv + pip install step (assumes you already activated an env with the demo deps). +.PARAMETER SkipPrerequisiteRegistration + Skip subscription-level provider and preview-feature checks. Use only after a subscription + administrator has confirmed that all prerequisites are registered. + .EXAMPLE ./quickstart.ps1 @@ -61,7 +65,8 @@ param( [string] $ExistingAoaiAccountName, [int] $Runs = 6, [switch] $SkipDemo, - [switch] $SkipPython + [switch] $SkipPython, + [switch] $SkipPrerequisiteRegistration ) $ErrorActionPreference = 'Stop' @@ -166,66 +171,70 @@ if ($NamePrefix -and ($NamePrefix -notmatch '^[a-z0-9]{3,12}$')) { # ----- 5. Provider + feature registration ----- Write-Step "Validating RP + preview-feature registration" -$checks = @( - @{ Kind='provider'; Namespace='Microsoft.Storage'; Name=$null } - @{ Kind='provider'; Namespace='Microsoft.CognitiveServices'; Name=$null } - @{ Kind='feature'; Namespace='Microsoft.CognitiveServices'; Name='OpenAI.ContextCacheAllowed' } -) - -function Get-RegState($c) { - if ($c.Kind -eq 'provider') { - return az provider show --namespace $c.Namespace --query registrationState -o tsv 2>$null - } - return az feature show --namespace $c.Namespace --name $c.Name --query properties.state -o tsv 2>$null -} +if ($SkipPrerequisiteRegistration) { + Write-Warn "Skipping subscription-level registration checks; assuming an administrator completed them." +} else { + $checks = @( + @{ Kind='provider'; Namespace='Microsoft.Storage'; Name=$null } + @{ Kind='provider'; Namespace='Microsoft.CognitiveServices'; Name=$null } + @{ Kind='feature'; Namespace='Microsoft.CognitiveServices'; Name='OpenAI.ContextCacheAllowed' } + ) -$toRegister = @() -foreach ($c in $checks) { - $state = Get-RegState $c - $label = if ($c.Kind -eq 'provider') { "provider $($c.Namespace)" } else { "feature $($c.Namespace)/$($c.Name)" } - if ($state -eq 'Registered') { - Write-Ok ("{0,-65} {1}" -f $label, $state) - } else { - Write-Warn ("{0,-65} {1}" -f $label, $(if ($state) { $state } else { 'NotRegistered' })) - $toRegister += $c + function Get-RegState($c) { + if ($c.Kind -eq 'provider') { + return az provider show --namespace $c.Namespace --query registrationState -o tsv 2>$null + } + return az feature show --namespace $c.Namespace --name $c.Name --query properties.state -o tsv 2>$null } -} -if ($toRegister.Count -gt 0) { - Write-Info "Registering missing items..." - foreach ($c in $toRegister) { - if ($c.Kind -eq 'provider') { - az provider register --namespace $c.Namespace | Out-Null + $toRegister = @() + foreach ($c in $checks) { + $state = Get-RegState $c + $label = if ($c.Kind -eq 'provider') { "provider $($c.Namespace)" } else { "feature $($c.Namespace)/$($c.Name)" } + if ($state -eq 'Registered') { + Write-Ok ("{0,-65} {1}" -f $label, $state) } else { - az feature register --namespace $c.Namespace --name $c.Name | Out-Null + Write-Warn ("{0,-65} {1}" -f $label, $(if ($state) { $state } else { 'NotRegistered' })) + $toRegister += $c } } - Write-Info "Waiting for all to reach 'Registered' (up to 10 min, refreshes every 20s)..." - $deadline = (Get-Date).AddMinutes(10) - do { - Start-Sleep -Seconds 20 - $stillPending = @() + + if ($toRegister.Count -gt 0) { + Write-Info "Registering missing items..." foreach ($c in $toRegister) { - $state = Get-RegState $c - $label = if ($c.Kind -eq 'provider') { "provider $($c.Namespace)" } else { "feature $($c.Namespace)/$($c.Name)" } - if ($state -ne 'Registered') { - $stillPending += "$label = $state" + if ($c.Kind -eq 'provider') { + az provider register --namespace $c.Namespace | Out-Null + } else { + az feature register --namespace $c.Namespace --name $c.Name | Out-Null } } - if ($stillPending.Count -eq 0) { break } - Write-Info ("[{0}] still pending: {1}" -f (Get-Date -Format HH:mm:ss), ($stillPending -join '; ')) - } while ((Get-Date) -lt $deadline) + Write-Info "Waiting for all to reach 'Registered' (up to 10 min, refreshes every 20s)..." + $deadline = (Get-Date).AddMinutes(10) + do { + Start-Sleep -Seconds 20 + $stillPending = @() + foreach ($c in $toRegister) { + $state = Get-RegState $c + $label = if ($c.Kind -eq 'provider') { "provider $($c.Namespace)" } else { "feature $($c.Namespace)/$($c.Name)" } + if ($state -ne 'Registered') { + $stillPending += "$label = $state" + } + } + if ($stillPending.Count -eq 0) { break } + Write-Info ("[{0}] still pending: {1}" -f (Get-Date -Format HH:mm:ss), ($stillPending -join '; ')) + } while ((Get-Date) -lt $deadline) - $finalPending = @() - foreach ($c in $toRegister) { - if ((Get-RegState $c) -ne 'Registered') { $finalPending += $c } - } - if ($finalPending.Count -gt 0) { - Write-Warn "Some items did not reach 'Registered' in time. Gated preview features may require allow-listing - email azurecontextcacherp@microsoft.com." - $cont = Read-NonEmpty "Continue with deployment anyway? (y/N)" 'N' - if ($cont -notmatch '^[Yy]') { throw "Aborted due to incomplete registration." } - } else { - Write-Ok "All providers and features are Registered." + $finalPending = @() + foreach ($c in $toRegister) { + if ((Get-RegState $c) -ne 'Registered') { $finalPending += $c } + } + if ($finalPending.Count -gt 0) { + Write-Warn "Some items did not reach 'Registered' in time. Gated preview features may require allow-listing - email azurecontextcacherp@microsoft.com." + $cont = Read-NonEmpty "Continue with deployment anyway? (y/N)" 'N' + if ($cont -notmatch '^[Yy]') { throw "Aborted due to incomplete registration." } + } else { + Write-Ok "All providers and features are Registered." + } } } @@ -302,10 +311,15 @@ if (-not (Test-Path (Join-Path $demoDir 'code_reviewer_demo.py'))) { Write-Step "Running keyless demo (DefaultAzureCredential, $Runs iterations)" -if (-not (Get-Command python -ErrorAction SilentlyContinue)) { - Write-Warn "python not in PATH; skipping demo. Install Python 3.10+ to run it." +$pythonCommand = Get-Command python3 -ErrorAction SilentlyContinue +if (-not $pythonCommand) { + $pythonCommand = Get-Command python -ErrorAction SilentlyContinue +} +if (-not $pythonCommand) { + Write-Warn "python3/python not in PATH; skipping demo. Install Python 3.10+ to run it." return } +$demoPython = $pythonCommand.Source Push-Location $demoDir try { @@ -313,12 +327,15 @@ try { $venv = Join-Path $demoDir '.venv' if (-not (Test-Path $venv)) { Write-Info "Creating venv at .venv ..." - python -m venv .venv + & $demoPython -m venv $venv + } + $venvPythonRelativePath = if ($IsWindows) { 'Scripts/python.exe' } else { 'bin/python' } + $demoPython = Join-Path $venv $venvPythonRelativePath + if (-not (Test-Path $demoPython)) { + throw "Virtual-environment Python not found at '$demoPython'. Remove '$venv' and re-run." } - $activate = Join-Path $venv 'Scripts\Activate.ps1' - . $activate Write-Info "Installing demo requirements (quiet)..." - pip install -q -r requirements.txt + & $demoPython -m pip install -q -r requirements.txt } $env:AOAI_ENDPOINT = $endpoint @@ -326,7 +343,7 @@ try { Remove-Item Env:AOAI_API_KEY -ErrorAction SilentlyContinue Write-Host "" - python code_reviewer_demo.py --aad --runs $Runs + & $demoPython code_reviewer_demo.py --aad --runs $Runs $demoExit = $LASTEXITCODE } finally { Pop-Location