Skip to content

Add retry logic for transient network errors in manage-payg-transition.ps1 - #1

Open
pochiraju wants to merge 48 commits into
masterfrom
rajpo_payg_updates
Open

Add retry logic for transient network errors in manage-payg-transition.ps1#1
pochiraju wants to merge 48 commits into
masterfrom
rajpo_payg_updates

Conversation

@pochiraju

Copy link
Copy Markdown
Owner

Summary

While running manage-payg-transition.ps1 outside the usual dev environment, some Azure CLI / Az PowerShell calls failed with transient network errors (Windows ephemeral port exhaustion / WinError 10048, HttpRequestException). This PR adds retry-with-backoff for those transient failures and fixes a correctness bug in the DataFactory SSIS section.

Changes

  • Added Invoke-AzCliArgsWithRetry (az CLI) and Invoke-AzCmdletWithRetry (Az PowerShell cmdlets) helpers that retry up to 3x with a 5s backoff, but only for transient error patterns (socket exhaustion/10048, connection reset, timeout, 429/5xx).
  • Wired the retry wrapper into Invoke-AzCliLicenseUpdate and Invoke-AzCliQuery.
  • Fixed the DataFactory SSIS Integration Runtime section: Set-AzContext, Get-AzDataFactoryV2, and Get-AzDataFactoryV2IntegrationRuntime previously had no -ErrorAction Stop, so a transient failure was silently treated as no integration runtimes found (and a failed context switch could leave later calls running against the wrong subscription). These now use -ErrorAction Stop + retry, and a genuine query failure is reported as UpdateResult = Failed in the CSV report instead of a false negative.

Validation

  • Full script and all three embedded sub-scripts (Azure/Arc/General) parse cleanly with the PowerShell language parser (no syntax errors).
  • Reproduced against a real run where ratruong-test-sqldb's update failed with WinError 10048; the failure path now retries automatically instead of failing permanently on the first transient blip.

/cc @twright-msft

pochiraju and others added 30 commits August 28, 2026 11:26
…sql-license-type.ps1

- Connect-Azure now reuses an existing valid Az/CLI session matching the
  target tenant instead of always forcing re-login, preventing hangs in
  non-interactive contexts.
- Module presence check now verifies Az.Accounts >= 4.2.0 directly instead
  of checking for the Az meta-package, avoiding false negatives and
  unnecessary/conflicting reinstalls.
- Includes pre-existing manage-payg-transition.ps1 fixes: removed unused
  Force_Start_On_Resources param usage, corrected Arc script download URL
  path, and cleaned up wrapper argument line-continuation formatting.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… but never fetched)

Live end-to-end testing of RunMode Single revealed the Arc/Azure sub-scripts
were never downloaded before being invoked via the generated wrapper,
causing 'term not recognized' errors. Invoke-RestMethod download calls
(matching Invoke-RemoteScript's Scheduled-mode logic) are now added before
building the wrapper lines for both Arc and Azure targets.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…e parameter

- Embedded the full logic of modify-azure-sql-license-type.ps1,
  modify-arc-sql-license-type.ps1, and set-azurerunbook.ps1 directly in this
  script. No external downloads (raw.githubusercontent.com) are required to
  run it anymore; the embedded content is materialized to local files at
  runtime (required for local invocation and Azure Automation runbook
  import), replacing the old Invoke-RestMethod download step.
- Added -TargetLicenseType parameter (PAYG [default] or AHUB) to control
  which license model resources are transitioned to. Internally translated
  to the vocabulary each embedded script expects:
    - Azure SQL resources: LicenseIncluded (PAYG) / BasePrice (AHUB)
    - Arc SQL Server: PAYG / LicenseOnly (AHUB-equivalent)
  Previously the Arc transition target was hardcoded to PAYG only.
- Updated README to describe the self-contained behavior and the new
  parameter.

Verified via live end-to-end testing against rajpoTest (SQL VM):
  - Default (-TargetLicenseType PAYG): AHUB -> PAYG succeeded, no downloads.
  - -TargetLicenseType AHUB: PAYG -> AHUB succeeded, no downloads.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…est results

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The embedded/standalone set-azurerunbook.ps1 hardcoded './PayTransitionDownloads/'
as the folder prefix for -RunbookPath during Import-AzAutomationRunbook, but the
outer manage-payg-transition.ps1 materializes embedded scripts to
'./manage-payg-transition/'. This mismatch caused every -RunMode Scheduled
invocation to fail because the runbook file could never be found at the
computed path.

Found via rubber-duck agent review.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- -Target now defaults to 'Both' instead of being mandatory.
- -RunMode now defaults to 'Single' instead of being mandatory.
- Updated docstring/examples to reflect the new defaults.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
These parameters are only actually used by the Azure Automation account setup
path (RunMode Scheduled). They were previously mandatory unconditionally, which
forced -RunMode Single (one-time run) callers to supply unused values. Made
both optional in the param block and added a runtime check that requires them
only when -RunMode is 'Scheduled', failing fast with a clear error otherwise.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…apper

The Single-mode command-line generator emitted boolean arg values (e.g. the
hardcoded Force = $true for Arc) as a separate quoted token: '-Force 'True''.
Since -Force is a [switch] parameter in the embedded script, PowerShell does
not bind that trailing 'True' token to the switch; instead it gets consumed as
an unbound positional argument, which binds to the script's first positional
parameter (-SubId). This caused failures such as:
  'Subscription True was not found in tenant ... Please verify that the
  subscription exists in this tenant.'

Fixed by detecting boolean-valued args and emitting them as a bare switch
(e.g. '-Force') with no value when true, and omitting them entirely when
false, instead of always emitting '-ArgName ''<value>'''.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The manage-payg-transition.ps1 wrapper exposed no -TenantId or -ReportOnly
parameters, so operators could not target a specific tenant explicitly or
perform a safe dry run. The embedded scripts already accept both; only the
wrapper pass-through was missing. Wired through for Single and Scheduled modes.

Also fixed a genuine bug in modify-arc-sql-license-type.ps1: the
'Found N resource(s) to update' message read $resources.Count before
$resources was populated by the paging loop, so it always reported 0 even
when resources were found. Moved the message after the loop and switched it
to $allResults.Count. Applied to both the standalone script and the copy
embedded in manage-payg-transition.ps1.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Set-AzConnectedMachineExtension is called with -NoWait and previously had no
-ErrorAction, so service-side failures (e.g. 'An extension of type ... is still
processing. Only one instance of an extension may be in progress at a time')
surfaced as non-terminating errors. The catch block was never entered, and the
script printed 'Updated -- ...' for resources that had in fact failed.

Add -ErrorAction Stop so those errors are caught, and record the real outcome
per resource in the CSV via new UpdateResult/UpdateError columns
(NotAttempted / RequestSubmitted / Failed). The error text is now included in
the console message as well.

Applied to both the standalone modify-arc-sql-license-type.ps1 and the copy
embedded in manage-payg-transition.ps1; verified the two blocks are identical.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The parameter table and all three examples documented parameters that do not
exist on manage-payg-transition.ps1 (-SubId, -ResourceGroup, -RunAt,
-AutomationAccount, -ExclusionTag). Every documented example would have failed
if a user copied it. Replaced with the real parameter names (-targetSubscription,
-targetResourceGroup, -RunMode, -AutomationAccResourceGroupName,
-AutomationAccountName) and documented the previously undocumented -Target,
-TenantId, -ReportOnly, -cleanDownloads.

Also:
- Added a dry-run example and documented -ReportOnly as the recommended first step.
- Documented implicit tenant selection and the new UpdateResult/UpdateError CSV columns.
- Noted that already-converged resources are excluded by design (idempotent re-runs)
  and that Disconnected/Expired Arc agents cannot be updated.
- Fixed Cloud Shell auth step to use Connect-AzAccount instead of Connect-AzureAD.

Verified every parameter appearing in a README example resolves against the
script's AST parameter block.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Each run materializes the embedded dependency scripts into
./manage-payg-transition/, writes a generated invocation wrapper to runnow.ps1,
and emits a ModifiedResources_<timestamp>.csv report plus a transcript log.
These were previously left untracked in the sample folder and could easily be
committed by accident.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolves the previously blocked Arc test case (microsoft#6) - Single/Arc has now been
executed end-to-end against real Arc-enabled machines. Adds cases microsoft#12-microsoft#19
covering the -Force switch-binding fix, -TenantId/-ReportOnly pass-through,
the always-zero resource count fix, isolated-folder self-containment, embedded
vs standalone sync, truthful Arc update outcomes, idempotent re-runs, and the
README parameter cross-check.

Refreshes Known gaps: Scheduled mode still never executed end-to-end;
Disconnected/Expired Arc agents cannot be updated; arcdata SqlServerInstances
with hostType 'Azure Virtual Machine' are read-only mirrors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Both license scripts called Start-Transcript unconditionally and matched it with
an unguarded Stop-Transcript. If transcription never started -- because the log
path is not writable, or because the host does not support transcription (Azure
Automation runbooks do not) -- the script ended with:

  Stop-Transcript: An error occurred stopping transcription: The host is not
  currently transcribing.

The run itself had already succeeded, so this surfaced a spurious failure at the
very end of an otherwise clean execution.

Track whether transcription actually started and only stop it in that case,
warning instead of throwing if the start fails. Applied to modify-arc-sql-license-type.ps1,
modify-azure-sql-license-type.ps1, and both copies embedded in manage-payg-transition.ps1.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
If the SQL server query returned nothing, the script fell back to scanning every
server in the subscription whenever -ResourceName was absent -- even when
-ResourceGroup had been supplied. The elastic pool query is not resource-group
filtered (it only filters on licenseType and tags), so a real run could have
modified elastic pools on servers entirely outside the requested resource group.
Observed live: a run scoped to 'rajpobuddy' went on to scan three servers in
PT_Bugbash_Databases, BinujRg and TestServersSi6ci1WestEuropeRG. No pools existed
there, so nothing was wrongly modified, but the exposure was real.

Only fall back to all servers when neither -ResourceName nor -ResourceGroup was
specified; otherwise skip database/elastic pool processing and say so.

Also fix two misleading messages that made a successful dry run look like a
no-op:
- The SQL VM path printed nothing in ReportOnly mode, so a resource found and
  recorded in the CSV appeared to be silently ignored. It now reports the
  transition it would have made.
- 'No SQL VMs found to start that require a license update' printed on every run
  because  is never populated. Reworded to describe what it
  actually means.

Applied to the standalone script and re-synced the embedded copy.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… verified end-to-end PAYG run

Case microsoft#21 records the scope-escape regression test: with -targetResourceGroup set
but no SQL Servers in that group, the script no longer falls back to scanning
every server in the subscription (which could have modified out-of-scope elastic
pools, since that query has no resource-group filter).

Case microsoft#22 records the final real run, with the resulting license type verified
out-of-band via 'az sql vm show' and Search-AzGraph rather than trusting the
script's own log output.

Also documents two newly-noted gaps: fixed transcript log paths overwrite prior
runs, and the Azure-side CSV lacks the UpdateResult/UpdateError columns.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The integration runtime filter selected any runtime with Type 'Managed' whose
LicenseType differed from the target. The default AutoResolveIntegrationRuntime
is also Type 'Managed' (it is the data-flow runtime, not an SSIS-IR) and has a
null LicenseType, so 'null -ne LicenseIncluded' selected it. The subsequent
Set-AzDataFactoryV2IntegrationRuntime round-tripped the full payload and the
service rejected it with:

  Conflict / DataFactoryPropertyUpdateNotSupported
  Updating property managedVirtualNetwork is not supported

The filter now also requires a non-empty LicenseType, which only SSIS
integration runtimes carry.

The failure was additionally reported as a success: the cmdlet had no
-ErrorAction Stop, so the Conflict was non-terminating, the enclosing catch
never fired, and the script printed '-- DataFactory ... updated to license type
LicenseIncluded' directly after two error blocks. Each runtime is now updated
inside its own try/catch with -ErrorAction Stop.

Applies the same result-checking to the SQL VM path, which piped 'az sql vm
update' straight into ConvertFrom-Json without checking LASTEXITCODE and
appended its CSV row before the attempt.

Rows are now projected onto an explicit column set at export time, because
Export-Csv derives its header from the first object only and would otherwise
drop UpdateResult/UpdateError depending on which section emitted first.

Adds TESTPLAN cases microsoft#23-microsoft#26 and re-syncs the embedded copy in the
self-contained orchestrator.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The result-checking added for SQL VMs covered only one of five 'az ... update'
call sites. Managed Instances, SQL Databases, elastic pools and instance pools
still piped the CLI output straight into ConvertFrom-Json without inspecting
LASTEXITCODE, and appended their CSV row before the attempt, so a failed update
was recorded identically to a successful one.

The elastic pool path was the worst case:

  az sql elastic-pool update ... 2>\ | ConvertFrom-Json -ErrorAction SilentlyContinue

which discarded the service error text entirely and reported only 'No result
returned', giving no indication of why the update failed.

All five paths now route through a shared Invoke-AzCliLicenseUpdate helper that
checks LASTEXITCODE, surfaces the real service error, and returns a result
object used to populate UpdateResult/UpdateError.

The helper initially wrote its success message with Write-Output from inside the
function, which in PowerShell merges into the return value: callers received a
two-element array rather than the result object, and the message never reached
the transcript. The helper is now silent on the success stream and each caller
logs its own line.

Adds TESTPLAN cases microsoft#27-microsoft#28 and re-syncs the embedded copy.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The report's UpdateResult column does not mean the same thing on both paths, and
nothing said so.

Azure CLI update calls are synchronous: no --no-wait is passed, so the CLI polls
the underlying PUT to a terminal state before returning. Measured on a SQL VM:
the call returned after 126s carrying provisioningState 'Succeeded', and the new
license type was immediately readable. 'Updated' therefore means committed.

The Arc path uses Set-AzConnectedMachineExtension -NoWait and never polls, so
'RequestSubmitted' means only that the service accepted the request. The
agent-side push can still fail afterwards without the script observing it.

Documents the distinction in the README, adds a Resource Graph query for
confirming the real Arc end state, and records TESTPLAN cases microsoft#29-microsoft#30 along with
the option of an opt-in -WaitForCompletion switch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Arc extension updates are submitted with -NoWait, so the report could only ever
record 'RequestSubmitted' - the service accepted the request, which says nothing
about whether the agent applied it. Confirming the real outcome previously
required a separate Resource Graph query.

Adds -WaitForCompletion (with -WaitTimeoutSeconds, default 300) which polls each
extension to a terminal provisioning state and records Succeeded/Failed/TimedOut
instead. The helper also compares the applied LicenseType against the requested
value, so a 'Succeeded' provisioning state carrying the wrong license is reported
as a failure rather than a success. Polling backs off 5->30s.

A timeout is deliberately not treated as a failure: the agent may still apply the
setting after the script stops waiting, so the outcome is recorded as
inconclusive.

The switch is opt-in because it serialises what is otherwise a fast parallel
fan-out across potentially hundreds of machines. Default behaviour is unchanged.

Verified live against sqltvm: reported Succeeded in 79.6s, confirmed
independently via Search-AzGraph (LicenseType LicenseOnly, provisioningState
Succeeded). Also verified inert under -ReportOnly and that the wrapper emits it
as a bare switch.

Adds TESTPLAN cases microsoft#31-microsoft#32.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Azure SQL updates previously always blocked until the operation completed, so a
large estate was processed serially. They are now submitted with --no-wait and
reported as 'RequestSubmitted'; passing -WaitForCompletion omits the flag so the
CLI polls to a terminal state and the result is reported as 'Updated'. This
matches the Arc path, which has always used -NoWait, giving one consistent
default across resource types.

Two resource types cannot honour this and are documented rather than faked:
'az sql vm update' and Set-AzDataFactoryV2IntegrationRuntime expose no
--no-wait/-AsJob equivalent. A generic 'az resource update/patch' fallback does
not support --no-wait either, and a direct ARM PATCH on a SQL VM is rejected
with MissingPatchParameters (approved values: tags, additionalVmPatch), so the
license type cannot be set by a lightweight non-blocking call. Hand-rolling a
full PUT was judged worse than documenting the limitation.

The helper only appends --no-wait for commands that accept it, and tolerates the
empty response body those calls return.

Adds TESTPLAN cases microsoft#33-microsoft#35 and re-syncs the embedded copy.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
'az sql vm update' has no --no-wait option and blocks for roughly two minutes
per VM, so SQL virtual machines were the one Azure resource type that ignored
the async-by-default behaviour introduced with -WaitForCompletion.

Update-AzSqlVM advertises -NoWait and -AsJob but both are broken in
Az.SqlVirtualMachine 2.4.0: -NoWait forwards the bound parameter into
Get-AzSqlVM, which rejects it, and -AsJob throws a NullReferenceException.

Add Invoke-SqlVmLicenseUpdate, which submits the change to ARM directly by
reading the resource and writing it back with only sqlServerLicenseType
changed. ARM accepts the request and returns an Azure-AsyncOperation header
without waiting, so the call completes in seconds. The synchronous CLI path is
retained for -WaitForCompletion and as an automatic fallback on failure.

Verified live against rajpoTest: default run reported RequestSubmitted in
49.7s and the change was confirmed in Azure; -WaitForCompletion reported
Updated in 189.3s. Re-synced the embedded orchestrator copy and corrected the
README behaviour matrix and TESTPLAN case microsoft#34, which wrongly recorded the SQL
VM limitation as unavoidable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When an Arc machine already carries a LicenseType it is only overwritten if
-Force is supplied. That is intended, but the run was silent about it: the
unrelated ConsentToRecurringPAYG block could still set WriteSettings, so the
script wrote other settings and printed 'Updated' and 'Confirmed ... Succeeded'
for a license type that had not changed. The wait helper could not detect this
because it is passed the unmodified settings value, so expected and applied
matched. Found while restoring sqltvm, which reported success yet remained
LicenseOnly.

Emit an explicit warning naming the current and requested license types and
pointing at -Force. -Force semantics are unchanged.

Also harden Wait-ArcExtensionProvisioning against a stale terminal state.
Updates are submitted with -NoWait, so the first poll can observe the previous
operation's 'Succeeded' before the new one begins, with the old license type
still in place, which was reported as a hard failure. A terminal state carrying
an unexpected license type is now inconclusive and polling continues; the
mismatch is only returned as Failed if it survives to the timeout. The observed
state sequence Updating -> Creating -> Succeeded over ~2.5 minutes confirms the
race window is real.

Verified live: without -Force the run now reports 'Write Settings - False' and
no success line; with -Force sqltvm was restored to PAYG and independently
confirmed via Get-AzConnectedMachineExtension. Embedded Arc block re-synced.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Discovery calls piped az straight into ConvertFrom-Json with no exit-code
check. The Azure CLI signals failure through \0 rather than by
throwing, so a failed query yielded \ and every caller read that as 'no
resources found'. A transient failure was therefore indistinguishable from an
empty result: the run printed 'Found a total of 0 databases' and 'No SQL
Databases found ... that require a license update' and moved on, silently
skipping resources that needed transitioning while still looking clean.

Observed in a real run where az sql db list returned ResourceGroupNotFound for
three servers. The errors were spurious: all four resource groups exist, one
server succeeded in the same resource group where another failed, and all three
listed their databases correctly on retry.

Route every discovery path through a new Invoke-AzCliQuery helper (SQL VM list,
VM power state, MI list, server list, database list, elastic pool list,
instance pool list). It checks the exit code, surfaces the service error as a
warning and normalises the value to an array. Callers that cannot proceed
without the data now skip with an explicit warning instead of reporting zero.
This is the same bug class already fixed for the update paths.

Verified: helper unit-tested across success, failure and empty; a re-run now
reports 1 database each for demo731 and sqldbinlinemigtestserver instead of 0.
Embedded orchestrator copy re-synced.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Every az call in the Azure script is scoped by the CLI's active subscription,
but 'az account set --subscription' was issued without checking \0
at either call site. If the switch failed, the whole subscription loop body,
including the update calls, would have continued against whichever subscription
was selected beforehand, so resources in an unintended subscription could have
been modified.

The primary call site now warns and moves to the next subscription. The
mid-loop re-selection throws instead, which the enclosing handler catches so
SQL Server, database and elastic pool processing is skipped without aborting
the run.

Audited the remaining unchecked 'az ... | ConvertFrom-Json' pipes: the az tag
list one sits inside a commented-out block and the rest are az account show
login probes that are already guarded.

Regression-verified with a ReportOnly run: all 6 servers and their per-server
database counts are still enumerated correctly. Embedded copy re-synced.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds test cases 43 and 44 covering a run that completed with no errors after
the discovery-query and subscription-context fixes.

Every 'nothing found' claim in that run was checked against Azure rather than
taken at face value: the two real databases are already at the target license
type and the rest are master databases with a null licenseType; the
subscription's single SQL VM and single managed instance are both already at
their target, so they are correctly excluded by the filters rather than
skipped; and there are no elastic pools, instance pools or Arc machines.

Case 44 validates the DataFactory SSIS filter at a scale not previously
tested: 22 integration runtimes across 8 factories, all with an empty
LicenseType, including 5 of type Managed. Under the original filter all 22
would have been selected and the Managed ones would have failed with the same
managedVirtualNetwork Conflict that motivated the fix.

Also narrows the async coverage gap note: bhrout-mi exists but is already at
the target and belongs to another team, so it was left alone.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
pochiraju and others added 18 commits August 28, 2026 21:45
Verified via 'git archive' that a merge containing only the
manage-payg-transition folder delivers a self-sufficient orchestrator:
it materializes both sub-scripts from its embedded here-strings and
runs end-to-end with the sibling scripts absent, and the materialized
copies are byte-identical to the fixed standalone scripts.

Records the caveat that a folder-only merge would leave the standalone
modify-azure-sql-license-type.ps1 and modify-arc-sql-license-type.ps1
stale on master and break the embedded-vs-standalone sync invariant.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Map -TargetLicenseType AHUB to 'Paid' (License with Software Assurance /
Azure Hybrid Benefit) for Arc SQL Server rather than 'LicenseOnly' (perpetual
without SA), allowing ESU to remain enabled and correctly aligning with AHB.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Implemented Format-ExecutionOutcomeSummary across Azure SQL, Arc SQL, and manage-payg-transition scripts.
- Summarizes qualified, updated, failed, and skipped resource counts grouped by friendly SQL resource type.
- Added detailed Failure & Skip Root Causes table reporting resource name, resource group, outcome, and diagnostic root cause reason.
- Updated TESTPLAN.md with test case microsoft#46 documenting the outcome summary and verification.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tatuses

- Wrapped Where-Object in @(...) to guarantee integer count in PowerShell.
- Added 'RequestSubmitted' and 'Succeeded' to the Azure script summary matching set.
- Verified live table formatting now correctly displays Updated counts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Renamed 'Updated' column to 'Updated or RequestSubmitted'.
- Set first column header to 'ResourceType' and sorted table by ResourceType.
- Consolidated summary output to print once at the very end of orchestrator execution instead of separate intermediate summaries.
- Updated sub-scripts to support -NoSummary when called by orchestrator.
- Updated TESTPLAN.md test case microsoft#46.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… summary

- Clean stale tracked_*.json files before starting Single run and after aggregating summary.
- Ensure sub-scripts remove tracked_*.json if no resources were marked for modification so prior runs do not contaminate subsequent runs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…1, modify-azure-sql-license-type.ps1, runnow.ps1)
…n.ps1

- Add Invoke-AzCliArgsWithRetry/Invoke-AzCmdletWithRetry helpers that retry
  transient network failures (socket exhaustion, HttpRequestException, timeouts)
  with backoff.
- Use retry wrapper in Invoke-AzCliLicenseUpdate and Invoke-AzCliQuery.
- Fix DataFactory SSIS section: Set-AzContext/Get-AzDataFactoryV2/
  Get-AzDataFactoryV2IntegrationRuntime now use -ErrorAction Stop + retry so a
  transient failure is no longer silently mistaken for 'no integration runtimes
  found' or left running against the wrong subscription context.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Get-AzSubscription -SubscriptionId / Get-AzSubscription had no
-ErrorAction Stop, so a transient HttpRequestException (seen repeatedly
outside the dev environment) was a non-terminating error: \
stayed empty, the foreach loop over subscriptions ran zero times, and the
script printed a clean-looking 'No resources were marked for
modification' summary instead of surfacing the failure. Fixed in both the
Arc and Azure sections: retry up to 3x with a 5s backoff, then exit 1
with a clear error if subscription resolution still fails, and abort
if the resolved subscription list is empty for any other reason.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sition blips

Real-world runs showed the Arc script exhausting 3 retries (5s each, ~10s
total) on 'Unable to acquire token ... An error occurred while sending
the request' immediately before the very next (Azure) invocation
succeeded with no retry needed at all, confirming these are short-lived
(10-30s) transient blips rather than a hard block. Increased to 5 attempts
with increasing backoff (5/10/20/30/30s, ~95s total) in both the Arc and
Azure sections so the script has a realistic chance to ride out the blip
instead of exiting before it clears.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Get-AzSubscription was being called without -TenantId, so for a
multi-tenant signed-in account (guest access to other tenants), it
fanned out and tried to acquire a token for *every* tenant the account
belongs to in order to resolve the subscription -> tenant mapping. This
produced unrelated 'Authentication failed against tenant ...
conditional access ... MFA' warnings for guest tenants even when a
specific -TenantId was passed to the script, and made the whole call
more fragile since it depended on tenants the user never intended to
touch. Added -TenantId \ to both Get-AzSubscription call sites
(single-subscription and list-all) in both the Arc and Azure sections,
scoping resolution strictly to the requested tenant.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…aph, az CLI)

- Arc section: auto-install Az.ConnectedMachine/Az.ResourceGraph on demand
  (mirroring the existing Az.DataFactory pattern) instead of silently
  continuing after a failed Import-Module, which left Search-AzGraph
  undefined and crashed later with an unrelated ArgumentNullException.
- Guard Search-AzGraph's result against \ before AddRange to avoid
  'Value cannot be null' if the module still can't be resolved.
- Azure section: fail fast with an actionable error if the 'az' CLI isn't
  installed, instead of letting every downstream az call fail later with
  a confusing 'term az is not recognized' error mid-run.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Attempts a silent 'winget install Microsoft.AzureCLI' when 'az' isn't on
PATH, refreshing the process PATH afterwards so a fresh install can be
picked up without restarting the shell. Falls back to the existing clear,
actionable error (with a manual install link) if winget isn't available
or the install fails.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sence

Replaces the single winget attempt with a real self-healing install path:
1. Try 'winget install Microsoft.AzureCLI' and verify az is now resolvable.
2. If winget is unavailable or didn't work, fall back to downloading and
   silently running the official Azure CLI MSI (aka.ms/installazurecliwindows)
   via msiexec, which doesn't depend on winget being present.
3. Only after both real install attempts fail does the script report the
   actionable manual-install error and exit - it no longer gives up after a
   single unverified winget call.

Adds a shared Refresh-Path helper to reload PATH from Machine/User scopes
after each install attempt so a freshly installed az.cmd is found in the
same process without restarting the shell.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a single pre-connect step in the top-level (non-embedded) part of the
script for -RunMode Single: establishes the Az PowerShell context and,
when targeting Azure, verifies/logs in the Azure CLI exactly once before
invoking the Arc and Azure sub-scripts.

Sets \ / \ so the
embedded Azure script's Connect-Azure short-circuits its own Azure CLI
install-check/login entirely when the parent already verified it for the
same tenant, instead of repeating that work. Falls back to the original
full connect logic unchanged when run standalone (e.g. as an Azure
Automation runbook) where these env vars won't be set.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Rewrite modify-azure-sql-license-type.ps1 (embedded) to use Az PowerShell
  cmdlets (Get-/Set-AzSqlVM/Instance/Database/ElasticPool/InstancePool,
  Set-AzDataFactoryV2IntegrationRuntime) instead of Azure CLI, addressing
  review feedback to drop the CLI dependency.
- Add explicit Az.Sql / Az.SqlVirtualMachine module ensure/install/import
  logic (previously relied on implicit autoloading, which fails when the
  modules are not already installed).
- SQL VM license updates remain fully synchronous (Update-AzSqlVM), since
  -NoWait/-AsJob are broken in Az.SqlVirtualMachine 2.4.0; other resource
  types keep the async default via -AsJob, mirroring the previous --no-wait
  behavior.
- Update README.md to reflect CLI-free prerequisites and current
  synchronous/async behavior per resource type.
- Update TESTPLAN.md with a new Round 2 section documenting live validation
  of the CLI removal across two subscriptions (SQL VM, Managed Instance,
  SQL Database), the Az.Sql/Az.SqlVirtualMachine regression and fix, and
  refresh the Known gaps / Required permissions sections for the
  PowerShell-only implementation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TESTPLAN.md records live test results against real Microsoft-internal
subscriptions, resource names, tenant IDs, and account details. Per Travis's
review feedback, remove it from source control and add it to .gitignore so
this internal information is not published in the public repo. The file
remains locally as a development record; it is simply no longer tracked.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Prompt before installing Az modules interactively; add -Force to skip prompts for non-interactive use
- Remove Arc/Azure distinction wording when confirming account/tenant context; confirm interactively or require -Force
- Simplify connection messages to 'Connecting to Azure...' and 'Reusing this session'
- Fix 'SQL Servers' -> 'SQL servers' for Azure SQL DB logical server references
- Remove stale 'mirrors the previous CLI' comments
- Replace 'RG' abbreviation with 'resource group' throughout output messages
- Remove SSIS/Data Factory integration runtime support entirely from the script and README
- Simplify README SQL VM synchronous-behavior explanation to one sentence

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant