Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
74 changes: 74 additions & 0 deletions scripts/tenant-remap-automated-deletion/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Automated Item Deletion Scripts

Set of PowerShell scripts that a tenant administrator can run to delete all Fabric items across all workspaces in a tenant. All Fabric items must be deleted before tenant remap, which moves a tenant's home region.

> The final two steps permanently delete your items, and you can't restore them. Confirm that you backed up all item definitions and data before you continue.

---

## What these scripts delete

| Deleted | Preserved |
|---|---|
| **Fabric items**: Lakehouses, Warehouses, Notebooks, Dataflows, Eventhouses, Eventstreams, and all other Fabric artifacts are **permanently deleted** | **Power BI items**: Semantic Models, Dashboards, Reports, and Paginated Reports are **preserved**|

---

## Usage

To clean up all Fabric items in your tenant, download `Step0.ps1` through `Step6.ps1` and follow these steps:

1. Sign in to the Global Admin account in Azure and open an Azure Cloud Shell session. Upload `Step0.ps1` through `Step6.ps1` to this session, and name them exactly as given.
1. Dot source each script by running the following command:

```powershell
. ./Step0.ps1; . ./Step1.ps1; . ./Step2.ps1; . ./Step3.ps1; . ./Step4.ps1; . ./Step5.ps1; . ./Step6.ps1
```

1. Run `Get-WorkspaceIds`. Note where `workspaceIds.txt`, `personalWorkspaceIds.txt`, and `sharedWorkspaceIds.txt` are stored.
1. Run `Restore-Workspaces -WorkspaceIdsFilePath workspaceIds.txt`. This command assumes that you're in the same folder as when you ran `Get-WorkspaceIds`. If not, make sure that `workspaceIds.txt`, `personalWorkspaceIds.txt`, and `sharedWorkspaceIds.txt` are in the same folder, and run the command again from that folder.
1. Open the Power BI Admin Portal and go to **Capacity settings**. Find a **Healthy** capacity and copy its ID. In the following command, replace `ID` with the copied ID, and then run the following command:

```powershell
Set-WorkspacesToCapacity -WorkspaceIdsFilePath workspaceIds.txt -CapacityId ID
```

If the capacity becomes full, find another healthy capacity and repeat the process until all workspaces are assigned to a capacity.
1. Run the following command:

```powershell
Add-AdminOnSharedWorkspaces -SharedWorkspaceIdsFilePath sharedWorkspaceIds.txt
```

1. Run the following command:

```powershell
Add-AdminOnPersonalWorkspaces -PersonalWorkspaceIdsFilePath personalWorkspaceIds.txt
```

1. Run `Remove-AllActiveArtifacts -WorkspaceIdsFilePath workspaceIds.txt`. This step permanently deletes your items, and you can't restore them after executing the command. When prompted, type the confirmation word `YES`.
1. Run `Remove-AllSoftDeletedArtifacts -WorkspaceIdsFilePath workspaceIds.txt`. When prompted, type the confirmation word `YES`.

---

## Output Files

| File | Step | Content |
|---|---|---|
| `workspaceIds.txt` | Step0 | List of all workspace IDs found in the tenant |
| `sharedWorkspaceIds.txt` | Step0 | List of all shared workspace IDs found |
| `personalWorkspaceIds.txt` | Step0 | List of all personal workspace IDs found |
| `workspace_$($wsId)_active_artifacts.json` | Step5 | List of active items (Power BI and Fabric) found |
| `workspace_$($wsId)_active_fabric_artifacts.json` | Step5 | List of active Fabric items found, targeted for deletion |
| `workspace_$($wsId)_softdeleted_artifacts.json` | Step6 | List of soft-deleted items (Power BI and Fabric) found |
| `workspace_$($wsId)_softdeleted_fabric_artifacts.json` | Step6 | List of soft-deleted Fabric items found, targeted for deletion |

---

## Troubleshooting

- **`Restore-Workspaces` reports workspace in Removing state**: No action is needed. The workspace is already being deleted by the system and can't be reliably restored, so let the system clean it up
- **`Set-WorkspacesToCapacity` errors on full capacity**: Find a different, healthy capacity with available space or create a new capacity. Assign remaining workspaces to this capacity
- **`Add-AdminOnPersonalWorkspaces` fails on non-429 error**: You may already be an admin in the workspace, or the workspace ID may be incorrect. Open the workspace directly in Fabric or Power BI to check. Also note that granting admin to a personal workspace is temporary (lasts for 24 hours), so you may need to run the script again
- **Remaining items after `Remove-AllActiveArtifacts`**: If there are remaining items reported in readiness checks after executing `Remove-AllActiveArtifacts`, make sure to run `Remove-AllSoftDeletedArtifacts` to clean up any remaining soft-deleted items. These soft-deleted items are in the recycle bin of each workspace
- **Remaining items after running all scripts**: These scripts perform best-effort deletion of items in the tenant. There may be remaining items, such as items in Admin Monitoring workspaces, that need to be deleted before passing readiness checks
69 changes: 69 additions & 0 deletions scripts/tenant-remap-automated-deletion/Step0.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#Requires -Modules Az.Accounts, Az.Resources

function Get-WorkspaceIds {
[CmdletBinding()]

param()

$SharedWorkspaceIdsFilePath = 'sharedWorkspaceIds.txt'
$PersonalWorkspaceIdsFilePath = 'personalWorkspaceIds.txt'
$AllWorkspaceIdsFilePath = 'workspaceIds.txt'

$ErrorActionPreference = 'Stop'

Set-StrictMode -Version Latest

if (($null -eq (Get-AzContext)) -or ((Get-AzContext).Account -like "MSI@*")) {
Connect-AzAccount -UseDeviceAuthentication | Out-Null
}

$secureFabricToken = (Get-AzAccessToken -ResourceUrl 'https://api.fabric.microsoft.com').Token
$ssPtr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureFabricToken)
$plainTextFabricToken = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ssPtr)

$h = @{ Authorization = "Bearer $plainTextFabricToken"; 'Content-Type' = 'application/json' }

try {
$nonPersonalWorkspaceIds = [System.Collections.Generic.List[string]]::new()
$personalWorkspaceIds = [System.Collections.Generic.List[string]]::new()

$uri = "https://api.fabric.microsoft.com/v1/admin/workspaces"
do {
$response = Invoke-RestMethod -Method GET -Uri $uri -Headers $h

foreach ($ws in $response.workspaces) {
switch ($ws.type) {
'Personal' {
$personalWorkspaceIds.Add($ws.id)
}
'Workspace' {
$nonPersonalWorkspaceIds.Add($ws.id)
}
default {
Write-Warning "Skipping workspace $($ws.id) with unrecognized type '$($ws.type)'"
}
}
}

$continuationToken = if ($response.PSObject.Properties.Name -contains 'continuationToken') { $response.continuationToken } else { $null }
if ($continuationToken) {
$uri = "https://api.fabric.microsoft.com/v1/admin/workspaces?continuationToken=$([System.Uri]::EscapeDataString($continuationToken))"
}
} while ($continuationToken)

Set-Content -Path $SharedWorkspaceIdsFilePath -Value $nonPersonalWorkspaceIds
Set-Content -Path $PersonalWorkspaceIdsFilePath -Value $personalWorkspaceIds
Set-Content -Path $AllWorkspaceIdsFilePath -Value ($nonPersonalWorkspaceIds + $personalWorkspaceIds)

Write-Host "Wrote $($nonPersonalWorkspaceIds.Count) shared or non-personal workspace ID(s) to $SharedWorkspaceIdsFilePath"
Write-Host "Wrote $($personalWorkspaceIds.Count) personal workspace ID(s) to $PersonalWorkspaceIdsFilePath"
Write-Host "Wrote $($nonPersonalWorkspaceIds.Count + $personalWorkspaceIds.Count) total workspace ID(s) to $AllWorkspaceIdsFilePath"
}
catch {
Write-Error "Error occurred: $($PSItem.Exception.Message)"
}
finally {
$plainTextFabricToken = [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ssPtr)
$h = $null
}
}
65 changes: 65 additions & 0 deletions scripts/tenant-remap-automated-deletion/Step1.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#Requires -Modules Az.Accounts, Az.Resources

function Restore-Workspaces {
[CmdletBinding()]

param(
[string]$AdminUpn=$null,
[Parameter(Mandatory=$true)]
[string]$WorkspaceIdsFilePath
)

$ErrorActionPreference = 'Stop'

Set-StrictMode -Version Latest

if ($WorkspaceIdsFilePath -ne $null -and (Test-Path -Path $WorkspaceIdsFilePath)) {
$workspaceIds = Get-Content -Path $WorkspaceIdsFilePath
}
else {
Write-Error "Workspace IDs file path is not provided or invalid. Please try again."
return
}

if (($null -eq (Get-AzContext)) -or ((Get-AzContext).Account -like "MSI@*")) {
Connect-AzAccount -UseDeviceAuthentication | Out-Null
}

if (-not $AdminUpn) { $AdminUpn = (Get-AzContext).Account.Id }

$adminOid = (Get-AzADUser -UserPrincipalName $AdminUpn).Id

$secureFabricToken = (Get-AzAccessToken -ResourceUrl 'https://api.fabric.microsoft.com').Token
$ssPtr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureFabricToken)
$plainTextFabricToken = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ssPtr)

$h = @{ Authorization = "Bearer $plainTextFabricToken"; 'Content-Type' = 'application/json' }

try {
foreach ($wsId in $workspaceIds) {
try {
$workspaceStatus = Invoke-RestMethod -Method GET -Uri "https://api.fabric.microsoft.com/v1/admin/workspaces/$wsId" -Headers $h
$workspaceDisplayName = $workspaceStatus.name
$workspaceState = $workspaceStatus.state

if ($workspaceState -eq 'Deleted') {
$body = @{ newWorkspaceAdminPrincipal = @{ id = $adminOid; type = 'User' }; 'newWorkspaceName' = "RestoredWorkspace_$($workspaceDisplayName)_$wsId" } | ConvertTo-Json -Depth 5

Invoke-RestMethod -Method POST -Uri "https://api.fabric.microsoft.com/v1/admin/workspaces/$wsId/restore" -Headers $h -Body $body
Write-Host "Restored inactive workspace, new workspace name: RestoredWorkspace_$($workspaceDisplayName)_$wsId"
}
elseif ($workspaceState -eq 'Removing') {
Write-Host "Workspace $wsId is in 'Removing' state, cannot reliably restore workspace as it is in the process of being permanently deleted"
}
}
catch {
Write-Warning "Error occurred: $($PSItem.Exception.Message)"
continue
}
}
}
finally {
$plainTextFabricToken = [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ssPtr)
$h = $null
}
}
59 changes: 59 additions & 0 deletions scripts/tenant-remap-automated-deletion/Step2.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#Requires -Modules Az.Accounts, Az.Resources

function Set-WorkspacesToCapacity {
[CmdletBinding()]

param(
[Parameter(Mandatory=$true)]
[string]$WorkspaceIdsFilePath,
[Parameter(Mandatory=$true)]
[string]$CapacityId
)

$ErrorActionPreference = 'Stop'

Set-StrictMode -Version Latest

if ($WorkspaceIdsFilePath -ne $null -and (Test-Path -Path $WorkspaceIdsFilePath)) {
$workspaceIds = Get-Content -Path $WorkspaceIdsFilePath
}
else {
Write-Error "Workspace IDs file path is not provided or invalid. Please try again."
return
}

if ($CapacityId -eq $null -or $CapacityId -eq '') {
Write-Error "Capacity ID is not provided or invalid. Please try again."
return
}

if (($null -eq (Get-AzContext)) -or ((Get-AzContext).Account -like "MSI@*")) {
Connect-AzAccount -UseDeviceAuthentication | Out-Null
}

$secureFabricToken = (Get-AzAccessToken -ResourceUrl 'https://api.fabric.microsoft.com').Token
$ssPtr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureFabricToken)
$plainTextFabricToken = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ssPtr)

$h = @{ Authorization = "Bearer $plainTextFabricToken"; 'Content-Type' = 'application/json' }

try {
foreach ($wsId in $workspaceIds) {
$ws = Invoke-RestMethod -Method GET -Uri "https://api.fabric.microsoft.com/v1/admin/workspaces/$wsId" -Headers $h

if (-not $ws.capacityId) {
Write-Host "[Assign Capacity] Assigning workspace $wsId to capacity $CapacityId"
$body = @{ capacityId = $CapacityId } | ConvertTo-Json

Invoke-RestMethod -Method POST -Uri "https://api.fabric.microsoft.com/v1/admin/workspaces/$wsId/assignToCapacity" -Headers $h -Body $body
}
}
}
catch {
Write-Error "Error occurred: $($PSItem.Exception.Message)"
}
finally {
$plainTextFabricToken = [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ssPtr)
$h = $null
}
}
63 changes: 63 additions & 0 deletions scripts/tenant-remap-automated-deletion/Step3.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#Requires -Modules Az.Accounts, Az.Resources

function Add-AdminOnSharedWorkspaces {
[CmdletBinding()]

param(
[string]$AdminUpn=$null,
[Parameter(Mandatory=$true)]
[string]$SharedWorkspaceIdsFilePath
)

$ErrorActionPreference = 'Stop'

Set-StrictMode -Version Latest

if ($SharedWorkspaceIdsFilePath -ne $null -and (Test-Path -Path $SharedWorkspaceIdsFilePath)) {
$sharedWorkspaceIds = Get-Content -Path $SharedWorkspaceIdsFilePath
}
else {
Write-Error "Shared workspace IDs file path is not provided or invalid. Please try again."
return
}

# authentication to get admin object ID for API calls
if (($null -eq (Get-AzContext)) -or ((Get-AzContext).Account -like "MSI@*")) {
# force device authentication if no account present or if using Managed Service ID (MSI)
Connect-AzAccount -UseDeviceAuthentication | Out-Null
}

if (-not $AdminUpn) { $AdminUpn = (Get-AzContext).Account.Id }

$adminOid = (Get-AzADUser -UserPrincipalName $AdminUpn).Id
$adminEmailAddress = (Get-AzADUser -UserPrincipalName $AdminUpn).mail

$secureFabricToken = (Get-AzAccessToken -ResourceUrl 'https://api.fabric.microsoft.com').Token
$ssPtr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureFabricToken)
$plainTextFabricToken = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ssPtr)

$h = @{ Authorization = "Bearer $plainTextFabricToken"; 'Content-Type' = 'application/json' }


try {
foreach ($wsId in $sharedWorkspaceIds) {
$users = Invoke-RestMethod -Method GET -Uri "https://api.powerbi.com/v1.0/myorg/admin/groups/$wsId/users" -Headers $h
$isAdmin = $users.value | Where-Object { $_.graphId -eq $adminOid -and $_.groupUserAccessRight -eq 'Admin' }

if ($isAdmin) { Write-Host "Already Admin on $wsId"; continue }

$body = @{ emailAddress = $adminEmailAddress; groupUserAccessRight = 'Admin' } | ConvertTo-Json -Depth 5

Invoke-RestMethod -Method POST -Uri "https://api.powerbi.com/v1.0/myorg/admin/groups/$wsId/users" -Headers $h -Body $body

Write-Host "Admin granted to $wsId via API"
}
}
catch {
Write-Error "Error occurred: $($PSItem.Exception.Message)"
}
finally {
$plainTextFabricToken = [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ssPtr)
$h = $null
}
}
Loading