Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
8aeba78
Fix auth-context reuse and Az.Accounts version check in modify-azure-…
pochiraju Aug 28, 2026
9e08b8a
Fix RunMode Single: download scripts before invoking (were referenced…
pochiraju Aug 28, 2026
8168ebe
Add test plan documenting validation of payg-transition fixes
pochiraju Aug 28, 2026
aa91ad1
Document required Azure RBAC permissions in TESTPLAN.md
pochiraju Aug 28, 2026
3ba2007
Add detailed per-resource-type RBAC permissions breakdown to README
pochiraju Aug 28, 2026
330ab7d
Make manage-payg-transition.ps1 self-contained; add -TargetLicenseTyp…
pochiraju Aug 28, 2026
02239d9
Update TESTPLAN.md with self-contained script and TargetLicenseType t…
pochiraju Aug 28, 2026
a557024
Fix Scheduled mode runbook import path mismatch
pochiraju Aug 28, 2026
75d45b4
Make Target and RunMode optional with defaults (Both/Single)
pochiraju Aug 28, 2026
cb150e5
Only require AutomationAccResourceGroupName/Location for Scheduled mode
pochiraju Aug 28, 2026
a5d4fed
Fix -Force switch being passed as '-Force ''True''' in Single-mode wr…
pochiraju Aug 28, 2026
98460c0
Add -TenantId/-ReportOnly pass-through and fix misleading resource count
pochiraju Aug 28, 2026
b24e886
Report actual per-resource update outcome in Arc license CSV
pochiraju Aug 28, 2026
50224d9
Correct README parameters to match the actual script
pochiraju Aug 28, 2026
49be41b
Ignore runtime artifacts generated by manage-payg-transition.ps1
pochiraju Aug 28, 2026
ad512d3
Update TESTPLAN with this session's test results and fixes
pochiraju Aug 28, 2026
d04e711
Guard Stop-Transcript so a failed Start-Transcript doesn't error
pochiraju Aug 28, 2026
a11b335
Add TESTPLAN case #20 for the Stop-Transcript guard
pochiraju Aug 28, 2026
0a35c4f
Don't widen scope when -ResourceGroup matches no SQL servers
pochiraju Aug 28, 2026
e8492fe
Add TESTPLAN cases #21-#22 for scope-escape fix and verified end-to-e…
pochiraju Aug 28, 2026
d919be6
Fix DataFactory license update selecting non-SSIS integration runtimes
pochiraju Aug 29, 2026
3123e4c
Report update outcomes for all Azure SQL resource types
pochiraju Aug 29, 2026
b7dfa99
Document synchronous Azure vs asynchronous Arc update completion
pochiraju Aug 29, 2026
015e554
Add opt-in -WaitForCompletion switch for Arc extension updates
pochiraju Aug 29, 2026
2e61bc0
Make non-blocking submission the default for Azure SQL updates
pochiraju Aug 29, 2026
a9ea2c1
Make SQL VM license updates asynchronous by default
pochiraju Aug 29, 2026
d4a8620
Stop reporting unchanged Arc license types as successful updates
pochiraju Aug 29, 2026
5f9a214
Stop treating failed discovery queries as empty results
pochiraju Aug 29, 2026
496c6b4
Skip a subscription when its CLI context cannot be selected
pochiraju Aug 29, 2026
2675436
Record verification of a clean run and the SSIS filter at scale
pochiraju Aug 29, 2026
85a3372
Document folder-scoped merge verification (test case 45)
pochiraju Aug 29, 2026
b4a49a9
Fix Arc SQL Server AHUB mapping to Paid instead of LicenseOnly
pochiraju Aug 31, 2026
6d53eab
Add execution outcome summary and failure root cause reporting
pochiraju Aug 31, 2026
4699303
Fix outcome summary Updated count matching across all update result s…
pochiraju Aug 31, 2026
d8c296a
Consolidate outcome summary at end of execution and update table schema
pochiraju Aug 31, 2026
2d2d817
Fix stale tracking file persistence in manage-payg-transition outcome…
pochiraju Aug 31, 2026
10ed4b4
Add uncommitted local scratch scripts (modify-arc-sql-license-type.ps…
pochiraju Aug 31, 2026
7b8fe75
Add retry logic for transient network errors in manage-payg-transitio…
pochiraju Sep 1, 2026
5c903b6
Retry and fail loudly on transient Get-AzSubscription errors
pochiraju Sep 1, 2026
bf665e6
Extend Get-AzSubscription retry backoff to survive longer token-acqui…
pochiraju Sep 1, 2026
455392f
Scope Get-AzSubscription to the requested tenant only
pochiraju Sep 1, 2026
cb40f22
Fix missing prerequisite handling (Az.ConnectedMachine, Az.ResourceGr…
pochiraju Sep 1, 2026
829e9cc
Auto-install Azure CLI via winget if missing before failing
pochiraju Sep 1, 2026
2c17540
Actually install Azure CLI instead of just detecting/reporting its ab…
pochiraju Sep 1, 2026
d5b09d1
Connect once in the parent script and pass it to Arc/Azure children
pochiraju Sep 1, 2026
2eb2123
Replace Azure CLI with Az PowerShell cmdlets in manage-payg-transition
pochiraju Sep 4, 2026
1709f37
Stop tracking TESTPLAN.md; ignore it going forward
pochiraju Sep 4, 2026
e1163bf
Address Travis's PR review comments
pochiraju Sep 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
777 changes: 777 additions & 0 deletions manage-payg-transition/modify-arc-sql-license-type.ps1

Large diffs are not rendered by default.

1,297 changes: 1,297 additions & 0 deletions manage-payg-transition/modify-azure-sql-license-type.ps1

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions runnow.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.\manage-payg-transition\modify-arc-sql-license-type.ps1 `
-UsePcoreLicense 'No' `
-ReportOnly `
-TenantId '72f988bf-86f1-41af-91ab-2d7cd011db47' `
-NoSummary `
-LicenseType 'PAYG' `
-SubId 'a5082b19-8a6e-4bc5-8fdd-8ef39dfebc39' `
-ResourceGroup 'rajpoArcEUSUSP' `
-Force
.\manage-payg-transition\modify-azure-sql-license-type.ps1 `
-LicenseType 'LicenseIncluded' `
-ResourceGroup 'rajpoArcEUSUSP' `
-SubId 'a5082b19-8a6e-4bc5-8fdd-8ef39dfebc39' `
-TenantId '72f988bf-86f1-41af-91ab-2d7cd011db47' `
-NoSummary `
-ReportOnly

Large diffs are not rendered by default.

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions samples/manage/manage-payg-transition/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Artifacts generated at runtime by manage-payg-transition.ps1.
# The embedded dependency scripts are materialized into ./manage-payg-transition/,
# the generated invocation wrapper is written to runnow.ps1, and each run emits a
# CSV report plus a PowerShell transcript. None of these belong in source control.
manage-payg-transition/
runnow.ps1
ModifiedResources_*.csv
*.log

# Internal test log used during development. It records live test results against
# real Microsoft internal subscriptions, resource names, tenant/subscription IDs
# and account details, and must not be published in this public repository.
TESTPLAN.md
158 changes: 128 additions & 30 deletions samples/manage/manage-payg-transition/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,28 @@ If not specified, all subscriptions your role has access to are scanned.
- You must have at least a *Contributor* RBAC role in each subscription you modify.
- You must have a *Tag Contributor* *Contributor* RBAC role in each subscription you modify.
- You must be connected to Azure AD and logged in to your Azure account. If your account have access to multiple tenants, make sure to log in with a specific tenant ID.
- The Az PowerShell modules `Az.Accounts`, `Az.Sql`, `Az.SqlVirtualMachine`,
`Az.ConnectedMachine`, and `Az.ResourceGraph` are required; the script installs any that
are missing automatically (for the current user, from the PowerShell Gallery). If you are
running the script interactively, it will ask for confirmation before installing a missing
module; pass `-Force` to install automatically without prompting (required for
non-interactive/unattended runs).

> [!NOTE]
> The Azure CLI (`az`) is **not** required. The script is implemented entirely with Az
> PowerShell cmdlets.

### Detailed permissions by resource type

*Contributor* is a superset of everything below and is the simplest option. If you
prefer a least-privilege role assignment instead, the dependent scripts require:

| Resource type modified | Built-in role needed |
|---|---|
| SQL Server VMs, Managed Instances, Azure SQL Databases, Elastic Pools, Instance Pools | *SQL DB Contributor* |
| Azure Arc-enabled SQL Server (Arc machine extensions) | *Azure Connected Machine Resource Administrator* |
| Reading/enumerating subscriptions and resources (all of the above) | *Reader* (included in every role above) |
| Tagging subscriptions with `ArcSQLServerExtensionDeployment:PAYG` | *Tag Contributor* |

---

Expand All @@ -28,32 +50,96 @@ The script accepts the following command line parameters:

| **Parameter**                                         | **Value**                                                                       | **Description** |
|:--|:--|:--|
|`-SubId`|`<subscription_id>` *or* `<file name>`|*Optional*: Subscription id or a .csv file with the list of subscriptions<sup>1</sup>. If not specified all subscriptions will be transitioned.|
|`-ResourceGroup` |`<name>`|*Optional*: Limits the scope of transition to a specified resource|
|`-RunAt` |`YYYY-MM-DD HH:MM:SS` |*Optional*: Sets the transition time in UTC time zone. E.g. 2025-05-01 14:00:00 means May 1, 2025 at 2pm UTC time. If not specified, the transition will be executed immediately.|
|`-Target`|`Arc`, `Azure`, `Both`|*Optional*. Which environment(s) to process. Defaults to `Both`.|
|`-RunMode`|`Single`, `Scheduled`|*Optional*. `Single` runs once and exits. `Scheduled` registers a recurring Azure Automation runbook. Defaults to `Single`.|
|`-targetSubscription`|`<subscription_id>`|*Optional*: Subscription id to limit the scope of the transition. If not specified, all subscriptions in the tenant will be transitioned.|
|`-targetResourceGroup` |`<name>`|*Optional*: Limits the scope of the transition to the specified resource group.|
|`-TenantId`|`<tenant_id>`|*Optional*. Azure AD tenant to operate against. If not specified, the tenant of the current Az PowerShell context (`(Get-AzContext).Tenant.Id`) is used. Specify explicitly to avoid running against whichever tenant happens to be selected in your session.|
|`-ReportOnly`|*(switch)*|*Optional*. Read-only dry run: reports the resources that would be changed without modifying anything.|
|`-WaitForCompletion`|*(switch)*|*Optional*. Wait for each license change to reach a terminal state and report a confirmed outcome. By default changes are submitted asynchronously and reported as `RequestSubmitted`. See [How It Works](#how-it-works).|
|`-UsePcoreLicense` | `Yes`, `No` |*Optional*. Passed to Arc script to control PCore licensing behavior. Set to `No` if not specified.|
|`-AutomationAccount`| `<name>`|*Required* if `-RunAt` is specified. The script will automatically create an automation account with this name unless one with this name alreday exists. It will be used for the “General” runbook import operation. |
|`-Location`|`<region>`|*Required* if `-RunAt` is specified. Azure region for the “General” runbook import operation.|
|`-ExclusionTag`|`<name:value>`|*Optional*. Specifies the tag name and value to exclude the tagged offline VMs from the forced activation during the transition |
|`-TargetLicenseType`|`PAYG`, `AHUB`|*Optional*. License type to transition resources to. Defaults to `PAYG`.|
|`-AutomationAccResourceGroupName`| `<name>`|*Required* only if `-RunMode Scheduled`. Resource group hosting the Automation Account, created if it does not already exist. Not used by `-RunMode Single`.|
|`-AutomationAccountName`| `<name>`|*Optional*. Name of the Automation Account used in `Scheduled` mode. Defaults to `aaccAzureArcSQLLicenseType`.|
|`-Location`|`<region>`|*Required* only if `-RunMode Scheduled`. Azure region for the Automation Account. Not used by `-RunMode Single`.|
|`-cleanDownloads`|`$true`, `$false`|*Optional*. Removes the `.\manage-payg-transition\` working folder after the run. Defaults to `$false`.|
|`-Force`|*(switch)*|*Optional*. Skip interactive confirmation prompts (installing missing Az modules, continuing with the current Azure account/tenant context). Required for non-interactive/unattended runs, where the script will otherwise throw an error instead of prompting.|

<sup>1</sup>You can create a .csv file using the following command and then edit to remove the subscriptions you don't want to scan.
```PowerShell
Get-AzSubscription | Export-Csv .\mysubscriptions.csv -NoTypeInformation
```
> [!NOTE]
> The script does not expose a `-SubId` parameter; use `-targetSubscription`. Scoping to a
> list of subscriptions from a `.csv` file is supported by the underlying
> `modify-azure-sql-license-type.ps1` / `modify-arc-sql-license-type.ps1` scripts when they
> are run directly, but is not passed through by this wrapper.


## How It Works

- This script internally runs the following scripts
- This script is **fully self-contained**: the logic of the following three scripts is
embedded directly in `manage-payg-transition.ps1` and requires no external downloads
to run:

`set-azurerunbook.ps1` - imports & publishes the helper runbook that and run if a scheduled execution is selected.

`modify-azure-sql-license-type.ps1` - configures the Azure SQL resources to pay-as-you-go

`modify-license-type.ps1` configures the existing Arc SQL resources to pay-as-you-go

- The dependent scripts are downloaded to `.\PaygTransitionDownloads\`. It is created automatically if doesn't exist. The downloaded scripts are refreshed automatically on each run to ensure that the up-to-date version is used.
- The offline Azure VMs will be reactivated for a brief period to change the configuration. If the VM should not be recativated, use `-ExclusionTag` option.
`modify-azure-sql-license-type.ps1` - configures the Azure SQL resources

`modify-arc-sql-license-type.ps1` - configures the existing Arc SQL resources

- At runtime, the embedded content of each script is written to local files under
`.\manage-payg-transition\` (created automatically if it doesn't exist) so it can be
invoked as a normal PowerShell script / imported as an Azure Automation runbook. No
network calls to GitHub are made to fetch these dependent scripts.
- Use `-TargetLicenseType` (`PAYG` by default, or `AHUB`) to control which license
model resources are transitioned to. This value is translated internally to the
vocabulary each embedded script expects (e.g. `LicenseIncluded`/`BasePrice` for Azure
SQL resources, `PAYG`/`Paid` for Arc SQL Server).
- Use `-ReportOnly` to perform a read-only dry run first. The script discovers and reports
every resource it would change (and writes a `ModifiedResources_<timestamp>.csv` report)
without modifying any license types. This is the recommended way to confirm the blast
radius before a real run.
- The script selects the tenant from `-TenantId` if supplied; otherwise it falls back to the
tenant of your current Az PowerShell context. Run `Get-AzContext` first, or pass
`-TenantId` explicitly, to be certain which tenant will be affected.
- Resources that already have the target license type are excluded from discovery by design,
so re-running the script is safe and idempotent. A converged environment correctly reports
`Found 0 resource(s) to update`.
- Arc-connected machines whose agent is `Disconnected` or `Expired` cannot be updated, because
the extension setting must be pushed to a reachable agent. These are skipped and will be
picked up on a later run once the machines reconnect.
- SQL virtual machines that are stopped/deallocated are **skipped** rather than modified —
the underlying VM must be running for `Update-AzSqlVM` to change its license type. These
are reported with `UpdateResult = SkippedNotRunning` and picked up automatically on a later
run once the VM is started.
- Each run writes a `ModifiedResources_<timestamp>.csv` report. The `UpdateResult` column
records the per-resource outcome and `UpdateError` carries the service error text when a
change was rejected.
- **By default the script does not wait for changes to finish.** Updates are submitted
asynchronously and the report records `RequestSubmitted`, which means *"the service accepted
the request"* — **not** *"the license type has changed"*. Pass `-WaitForCompletion` to wait
for each change to reach a terminal state and report a confirmed outcome instead.

| Resource | Default | With `-WaitForCompletion` |
|---|---|---|
| SQL Managed Instance, database, elastic pool, instance pool | `-AsJob`, reports `RequestSubmitted` | waits, reports `Updated` |
| Arc-connected machine | `-NoWait`, reports `RequestSubmitted` | polls the extension, reports `Succeeded` / `Failed` / `TimedOut` |
| **SQL virtual machine** | **always waits**, reports `Updated` | same |

SQL VM updates are always synchronous, regardless of `-WaitForCompletion`.

For Arc, a `TimedOut` result is inconclusive rather than a failure — the agent may still
apply the setting after the script stops waiting.
- To confirm outcomes after a default (non-waiting) run, either re-run the script — already
converged resources are excluded by discovery, so a second run reports only what genuinely
still needs changing — or query the current state directly. For Arc:

```powershell
Search-AzGraph -Query @"
resources
| where type =~ 'microsoft.hybridcompute/machines/extensions'
| where properties.type in~ ('WindowsAgent.SqlServer','LinuxAgent.SqlServer')
| project name = split(id,'/')[8], licenseType = properties.settings.LicenseType,
state = properties.provisioningState
"@
```
- The subscriptions in scope of the transition will be automatically tagged with `ArcSQLServerExtensionDeployment:PAYG` to ensure that the furure SQL Servers onboarded to Azure Arc are configured to use the pay-as-you-go subscription. For details, see [Manage automatic connection for SQL Server enabled by Azure Arc](https://learn.microsoft.com/sql/sql-server/azure-arc/manage-autodeploy).

## Example 1
Expand All @@ -62,29 +148,41 @@ Switch all machines to pay-as-you-go in a single subscription immediately and us

```powershell
.\manage-payg-transition.ps1 `
-SubId "00000000-0000-0000-0000-000000000000" `
-UsePcoreLicense Yes
-targetSubscription "00000000-0000-0000-0000-000000000000" `
-UsePcoreLicense Yes
````

## Example 2

Preview (dry run) what would change across an entire tenant, without modifying anything. This is the recommended first step before any real run.

```powershell
.\manage-payg-transition.ps1 `
-TenantId "00000000-0000-0000-0000-000000000000" `
-ReportOnly
````

## Example 2
## Example 3

Switch all machines to pay-as-you-go in subscriptions listed in MySusbcriptions.csv immediately without using unlimited virtualization. Exclude the VMs that tagged with `DoNotActivate:True`
Switch the machines in a single resource group back to Azure Hybrid Benefit (AHUB).

```powershell
.\manage-payg-transition.ps1 `
-SubId MySubscription.csv
-ExclusionTag DoNotActivate:True
-targetSubscription "00000000-0000-0000-0000-000000000000" `
-targetResourceGroup "MyResourceGroup" `
-TargetLicenseType AHUB
````

## Example 3
## Example 4

Switch all machines to pay-as-you-go in *all* subscriptions on May 1, 2025 at 0:00 using an automation account `MyAutomation` in `EatUS` region.
Schedule a recurring daily transition for Azure resources only, using an automation account in the `EastUS` region.

```powershell
.\manage-payg-transition.ps1 `
-SubId "00000000-0000-0000-0000-000000000000" `
-RunAt "2025-05-01 00:00:00"
-AutomationAccount MyAutomation
-Target Azure `
-RunMode Scheduled `
-AutomationAccResourceGroupName "MyAutomationRG" `
-AutomationAccountName "MyAutomation" `
-Location "EastUS"
```
# Running the script using Cloud Shell
Expand All @@ -93,10 +191,10 @@ This option is recommended because Cloud shell has the Azure PowerShell modules

1. Launch the [Cloud Shell](https://shell.azure.com/). For details, [read more about PowerShell in Cloud Shell](https://aka.ms/pscloudshell/docs).

1. Connect to Azure AD. You must specify `<tenant_id>` if you have access to more than one AAD tenants.
1. Connect to Azure. You must specify `<tenant_id>` if you have access to more than one AAD tenant.

```console
Connect-AzureAD -TenantID <tenant_id>
Connect-AzAccount -TenantId <tenant_id>
```

1. Upload the script to your cloud shell using the following command:
Expand Down
Loading