├── README.md ├── SampleReport.pbix ├── .github └── FUNDING.yml ├── Admin-TenantScan.pbit ├── Admin-ActivityScan.pbit ├── Admin-RefreshHistory.pbit ├── SampleReport - Live.pbix ├── lib ├── Microsoft.AnalysisServices.AdomdClient.dll └── Microsoft.AnalysisServices.SPClient.Interfaces.dll ├── .gitignore ├── Gateways-List.ps1 ├── Dataset-GetDatasets.ps1 ├── Dataset-GetScheduleRefresh - Admin.ps1 ├── Dataset-GetUsersFromDataset.ps1 ├── Admin-GetApps.ps1 ├── XMLA - Model - Deploy.ps1 ├── Apps - ListAppsUsers.ps1 ├── Workspace - AddUserAsAdmin.ps1 ├── Dataset-GetScheduleRefresh.ps1 ├── GetToken-MSAL.ps1 ├── Security - GetLinksSharedToWholeOrganization.ps1 ├── Security - GetUserArtifactAccess.ps1 ├── XMLA - AMO - Sample.ps1 ├── GetToken-MSALDeviceCodeAuth.ps1 ├── LICENSE ├── Dataset-RefreshSync.ps1 ├── Dataset-ListDatasources.ps1 ├── Report-Rebind-Manual.ps1 ├── Dataset-UpdateCredentials.ps1 ├── PaginatedReport-Export.ps1 ├── Admin-GetGroupsAsAdmin.ps1 ├── Admin-O365AuditLog.ps1 ├── Workspace-SearchAndDelete.ps1 ├── Dataset-ExecuteXMLAQuery.ps1 ├── Dataset-GetRefreshHistory-Dataset.ps1 ├── Dataset-RefreshAsync-Cancel.ps1 ├── Dataset-QSO.ps1 ├── XMLA - Partitions - Process (Manual).ps1 ├── Report-ExportPowerBIReport.ps1 ├── Admin-AddUserToWorkspace.ps1 ├── Dataset-ExecuteDAXQuery.ps1 ├── Report-Rebind-Auto (Non Admin).ps1 ├── API-UserApis.ps1 ├── MicrosoftPowerBIMgmt-Demo.ps1 ├── Report-Rebind-Auto (Admin).ps1 ├── Report-Rebind-AllReportsFromWorkspaceToDataset.ps1 ├── Workspace-Premium Capacity Switch.ps1 ├── XMLA - Partitions - Process (Unprocessed).ps1 ├── Admin-WorkspaceScan.ps1 ├── Dataset-RefreshAsync.ps1 ├── Security - EnsureUserOnAllWorkspaces.ps1 ├── Report-PublishReport.ps1 ├── Admin-ActivityScan.ps1 ├── XMLA - Model - Replicate.ps1 ├── Realtime - Streaming Dataset.ps1 ├── API-ListSensitivityLabels.ps1 ├── Dataset-DeployDatasetGTWBind.ps1 ├── Realtime - Push Dataset.ps1 ├── Admin-RefreshHistory.ps1 ├── XMLA - ExecuteQuery.ps1 ├── Hack - FixReportConnections.ps1 ├── Workspace-Clone.ps1 ├── Gateways-AddDatasource.ps1 ├── XMLA - Partitions - Create.ps1 ├── Admin-TenantScan.ps1 └── TOMHelper.psm1 /README.md: -------------------------------------------------------------------------------- 1 | # pbiscripts -------------------------------------------------------------------------------- /SampleReport.pbix: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuiRomano/pbiscripts/HEAD/SampleReport.pbix -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: RuiRomano 4 | -------------------------------------------------------------------------------- /Admin-TenantScan.pbit: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuiRomano/pbiscripts/HEAD/Admin-TenantScan.pbit -------------------------------------------------------------------------------- /Admin-ActivityScan.pbit: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuiRomano/pbiscripts/HEAD/Admin-ActivityScan.pbit -------------------------------------------------------------------------------- /Admin-RefreshHistory.pbit: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuiRomano/pbiscripts/HEAD/Admin-RefreshHistory.pbit -------------------------------------------------------------------------------- /SampleReport - Live.pbix: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuiRomano/pbiscripts/HEAD/SampleReport - Live.pbix -------------------------------------------------------------------------------- /lib/Microsoft.AnalysisServices.AdomdClient.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuiRomano/pbiscripts/HEAD/lib/Microsoft.AnalysisServices.AdomdClient.dll -------------------------------------------------------------------------------- /lib/Microsoft.AnalysisServices.SPClient.Interfaces.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RuiRomano/pbiscripts/HEAD/lib/Microsoft.AnalysisServices.SPClient.Interfaces.dll -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.url 2 | 3 | shareddatasets.json 4 | 5 | datasetlineage.json 6 | /relatedcontent.json 7 | 8 | /output/* 9 | 10 | _temp/ 11 | 12 | /nuget/* 13 | *.pbix 14 | -------------------------------------------------------------------------------- /Gateways-List.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 4 | 5 | Connect-PowerBIServiceAccount 6 | 7 | $result = Invoke-PowerBIRestMethod -url "gateways" -method Get | ConvertFrom-Json 8 | 9 | $result.value | Out-String 10 | -------------------------------------------------------------------------------- /Dataset-GetDatasets.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 4 | 5 | Connect-PowerBIServiceAccount 6 | 7 | $result = Invoke-PowerBIRestMethod -url "admin/datasets" -method Get | ConvertFrom-Json 8 | 9 | $result.value | Out-String 10 | -------------------------------------------------------------------------------- /Dataset-GetScheduleRefresh - Admin.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 4 | 5 | Connect-PowerBIServiceAccount 6 | 7 | $result = Invoke-PowerBIRestMethod -url "admin/capacities/refreshables" -method Get | ConvertFrom-Json | select -ExpandProperty value 8 | 9 | $result | Format-Table -------------------------------------------------------------------------------- /Dataset-GetUsersFromDataset.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 4 | 5 | $datasetId = "33fc0a15-4be2-4f02-85fd-a3f2e9fdec8c" 6 | 7 | Connect-PowerBIServiceAccount 8 | 9 | $result = Invoke-PowerBIRestMethod -url "admin/datasets/$datasetId/users" -method Get | ConvertFrom-Json | Select -ExpandProperty value 10 | 11 | $result -------------------------------------------------------------------------------- /Admin-GetApps.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1077" } 2 | 3 | 4 | $ErrorActionPreference = "Stop" 5 | 6 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 7 | 8 | Set-Location $currentPath 9 | 10 | try { 11 | $token = Get-PowerBIAccessToken 12 | } 13 | catch { 14 | $pbiAccount = Connect-PowerBIServiceAccount 15 | } 16 | 17 | $result = Invoke-PowerBIRestMethod -Url "admin/apps?`$top=1000" -Method Get | ConvertFrom-Json | select -ExpandProperty value 18 | 19 | $result | Format-List -------------------------------------------------------------------------------- /XMLA - Model - Deploy.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | $serverName = "powerbi://api.powerbi.com/v1.0/myorg/Session%20-%20PBI%20on%20Steroids" 3 | , $databaseName = "WWI - Sales (Partitioned)" 4 | , $bimFilePath = ".\SampleModel.bim" 5 | ) 6 | 7 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 8 | 9 | Set-Location $currentPath 10 | 11 | Import-Module "$currentPath\TOMHelper.psm1" -Force 12 | 13 | Update-ASDatabase -serverName $serverName -databaseName $databaseName -bimFilePath $bimFilePath -deployPartitions:$true -deployRoles:$false -deployConnections:$false 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Apps - ListAppsUsers.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | cls 4 | 5 | # Get the authentication token using ADAL library (OAuth) 6 | 7 | Connect-PowerBIServiceAccount 8 | 9 | # Artifacts 10 | 11 | $result = Invoke-PowerBIRestMethod -method Get -url "admin/apps?`$top=1000" | ConvertFrom-Json | Select -ExpandProperty value 12 | 13 | $result | Format-Table 14 | 15 | Write-Host "App Users" 16 | 17 | foreach ($app in $result) 18 | { 19 | Write-Host "App '$($app.id)' users:" 20 | 21 | $users = Invoke-PowerBIRestMethod -method Get -url "admin/apps/$($app.id)/users" | ConvertFrom-Json | Select -ExpandProperty value 22 | 23 | $users | Format-Table 24 | } -------------------------------------------------------------------------------- /Workspace - AddUserAsAdmin.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | cls 4 | 5 | # Object Id from Azure AD 6 | $workspaceId = "f5e73633-29c1-432f-ad5f-3ef5863dcaec" 7 | 8 | #https://docs.microsoft.com/en-us/rest/api/power-bi/groups/update-group-user#groupuseraccessright 9 | $principalObj = @{ 10 | 11 | identifier = "2eab306a-bbd8-4f14-8793-eea2d1589585"; 12 | groupUserAccessRight = "Member"; 13 | principalType = "Group" 14 | } 15 | 16 | # Get the authentication token using ADAL library (OAuth) 17 | 18 | Connect-PowerBIServiceAccount 19 | 20 | $body = $principalObj | ConvertTo-Json 21 | 22 | Invoke-PowerBIRestMethod -Url "groups/$workspaceId/users" -Method Post -Body $body 23 | -------------------------------------------------------------------------------- /Dataset-GetScheduleRefresh.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 4 | 5 | $workspaceId = "e25577b9-1f8e-4bb3-8fa7-73c35e067a06" 6 | $datasetId = "5833cb8c-458a-4876-b1b0-ef3fe4c11e4e" 7 | 8 | Connect-PowerBIServiceAccount 9 | 10 | $result = Invoke-PowerBIRestMethod -url "groups/$workspaceId/datasets/$datasetId/refreshes" -method Get | ConvertFrom-Json 11 | 12 | Write-Host "Latest Refresh" 13 | 14 | $result.value | Format-Table 15 | 16 | $result = Invoke-PowerBIRestMethod -url "groups/$workspaceId/datasets/$datasetId/refreshSchedule" -method Get | ConvertFrom-Json 17 | 18 | Write-Host "Refresh Schedule" 19 | 20 | $result | Out-String 21 | -------------------------------------------------------------------------------- /GetToken-MSAL.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules "MSAL.PS" 2 | 3 | cls 4 | 5 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 6 | 7 | $clientId = "" 8 | $tenantId = "common" 9 | $authority = "https://login.microsoftonline.com/$tenantId" 10 | $apiResource = "https://analysis.windows.net/powerbi/api" 11 | 12 | $scopes = @("$apiResource/.default") 13 | 14 | $app = New-MsalClientApplication -ClientId $clientId -Authority $authority -TenantId $tenantId 15 | 16 | $oauthResult = $app | Get-MsalToken -Scopes $scopes 17 | 18 | $accessToken = $oauthResult.AccessToken 19 | 20 | $headers = @{ 21 | 'Content-Type'= "application/json" 22 | 'Authorization'= "Bearer $accessToken" 23 | } 24 | 25 | $response = Invoke-RestMethod -Uri "https://api.powerbi.com/v1.0/myorg/groups" -Headers $headers -Method Get 26 | 27 | $response.value -------------------------------------------------------------------------------- /Security - GetLinksSharedToWholeOrganization.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | cls 4 | 5 | # Get the authentication token using ADAL library (OAuth) 6 | 7 | Connect-PowerBIServiceAccount 8 | 9 | # Artifacts 10 | 11 | $result = Invoke-PowerBIRestMethod -method Get -url "admin/widelySharedArtifacts/linksSharedToWholeOrganization" | ConvertFrom-Json 12 | 13 | $artifactAccessEntities = @() 14 | 15 | $artifactAccessEntities += @($result.ArtifactAccessEntities) 16 | 17 | while($result.continuationToken -ne $null) 18 | { 19 | $result = Invoke-PowerBIRestMethod -Url $result.continuationUri -method Get | ConvertFrom-Json 20 | 21 | if ($result.ArtifactAccessEntities) 22 | { 23 | $artifactAccessEntities += @($result.ArtifactAccessEntities) 24 | } 25 | } 26 | 27 | $artifactAccessEntities | Format-Table -------------------------------------------------------------------------------- /Security - GetUserArtifactAccess.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | cls 4 | 5 | # Object Id from Azure AD 6 | $userGraphId = "1e399999-ee51-4ebb-9c84-7adb562d1074" 7 | 8 | # Get the authentication token using ADAL library (OAuth) 9 | 10 | Connect-PowerBIServiceAccount 11 | 12 | # Artifacts 13 | 14 | $result = Invoke-PowerBIRestMethod -method Get -url "admin/users/$userGraphId/artifactAccess" | ConvertFrom-Json 15 | 16 | $artifactAccessEntities = @() 17 | 18 | $artifactAccessEntities += @($result.ArtifactAccessEntities) 19 | 20 | while($result.continuationToken -ne $null) 21 | { 22 | $result = Invoke-PowerBIRestMethod -Url $result.continuationUri -method Get | ConvertFrom-Json 23 | 24 | if ($result.ArtifactAccessEntities) 25 | { 26 | $artifactAccessEntities += @($result.ArtifactAccessEntities) 27 | } 28 | } 29 | 30 | $artifactAccessEntities | Format-Table -------------------------------------------------------------------------------- /XMLA - AMO - Sample.ps1: -------------------------------------------------------------------------------- 1 | cls 2 | 3 | $serverName = "Data Source=powerbi://api.powerbi.com/v1.0/myorg/Contoso" 4 | 5 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 6 | 7 | Import-Module "$currentPath\TOMHelper.psm1" -Force 8 | 9 | $asModelType = [Microsoft.AnalysisServices.Tabular.Model] 10 | 11 | $assembly = $asModelType.Assembly 12 | 13 | Write-Host "Assembly version loaded: '$($assembly.FullName)' from '$($assembly.Location)'" 14 | 15 | try 16 | { 17 | Write-Host "Connecting to Server: '$serverName'" 18 | 19 | $server = New-Object Microsoft.AnalysisServices.Tabular.Server 20 | 21 | $server.Connect($serverName) 22 | 23 | Write-Host "Connected" 24 | 25 | $tables = $server.Databases[0].Model.Tables 26 | 27 | foreach($table in $tables) 28 | { 29 | Write-Host "$($table.Name)" 30 | } 31 | 32 | } 33 | finally 34 | { 35 | if($server) 36 | { 37 | $server.Dispose() 38 | } 39 | } 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /GetToken-MSALDeviceCodeAuth.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules "MSAL.PS" 2 | 3 | cls 4 | 5 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 6 | 7 | $clientId = "" 8 | $authority = "https://login.microsoftonline.com/common" 9 | $apiResource = "https://analysis.windows.net/powerbi/api" 10 | 11 | $scopes = @("$apiResource/.default") 12 | 13 | $app = New-MsalClientApplication -ClientId $clientId -Authority $authority 14 | 15 | # Warning - Dont work with PowerShell 7, must use powershell5 16 | 17 | Enable-MsalTokenCacheOnDisk $app 18 | 19 | Write-Host "Cache file: $([TokenCacheHelper]::CacheFilePath)" 20 | 21 | try { 22 | $token = $app | Get-MsalToken -Scopes $scopes -Silent 23 | } 24 | catch { 25 | Write-Host "Getting token" 26 | 27 | $token = $app | Get-MsalToken -Scopes $scopes -DeviceCode -Silent 28 | } 29 | 30 | $headers = @{ 31 | 'Content-Type'= "application/json" 32 | 'Authorization'= "Bearer $($token.AccessToken)" 33 | } 34 | 35 | $response = Invoke-RestMethod -Uri "https://api.powerbi.com/v1.0/myorg/admin/apps?`$top=100" -Headers $headers -Method Get -ContentType $contentType 36 | 37 | $response.value -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Rui Romano 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Dataset-RefreshSync.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | param( 4 | $workspaceId = "279de5c8-f502-43e7-8b65-5e9ebd6f9434" 5 | , 6 | $datasetId = "faf7eff7-413e-49e1-ba98-b1daa03a52d9" 7 | ) 8 | 9 | $executeJsonObj = @{ 10 | "notifyOption" = "MailOnCompletion" 11 | } 12 | 13 | $executeJsonBody = $executeJsonObj | ConvertTo-Json -Depth 5 14 | 15 | Connect-PowerBIServiceAccount 16 | 17 | Write-Host "Posting a new Refresh Command" 18 | 19 | Invoke-PowerBIRestMethod -url "groups/$workspaceId/datasets/$datasetId/refreshes" -method Post -Body $executeJsonBody 20 | 21 | Write-Host "Waiting for refresh to end" 22 | 23 | do 24 | { 25 | $refreshes = Invoke-PowerBIRestMethod -url "groups/$workspaceId/datasets/$datasetId/refreshes?`$top=1" -method Get | ConvertFrom-Json | select -ExpandProperty value 26 | 27 | Write-Host "sleeping..." 28 | 29 | Start-Sleep -Seconds 2 30 | 31 | } 32 | 33 | while($refreshes.status -iin @("Unknown", "inProgress", "notStarted")) 34 | 35 | Write-Host "Refresh complete: $((([datetime]$refreshes.endTime) - ([datetime]$refreshes.startTime)).TotalSeconds)s" -------------------------------------------------------------------------------- /Dataset-ListDatasources.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | param( 4 | $workspaceId = "cdee92d2-3ff9-43e2-9f71-0916e888ad27", 5 | $datasetId = "33fc0a15-4be2-4f02-85fd-a3f2e9fdec8c" 6 | ) 7 | 8 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 9 | 10 | try { Get-PowerBIAccessToken | out-null } catch { Connect-PowerBIServiceAccount } 11 | 12 | Write-Host "Getting Gateways" 13 | 14 | $gateways = Invoke-PowerBIRestMethod -url "gateways" -method Get | ConvertFrom-Json | Select -ExpandProperty value 15 | 16 | $datasources = @(Invoke-PowerBIRestMethod -url "groups/$workspaceId/datasets/$datasetId/datasources" -method Get | ConvertFrom-Json | Select -ExpandProperty value) 17 | 18 | $datasources | Format-List 19 | 20 | $gatewayId = $datasources | select -First 1 -ExpandProperty gatewayId 21 | 22 | $datasourcesIdsToMatch = @($datasources.datasourceId) 23 | 24 | $gwDatasources = Invoke-PowerBIRestMethod -url "gateways/$gatewayId/datasources" -method Get | ConvertFrom-Json | Select -ExpandProperty value 25 | 26 | $gwDatasources |? { $datasourcesIdsToMatch -contains $_.id } | Format-List 27 | -------------------------------------------------------------------------------- /Report-Rebind-Manual.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | param( 4 | $reports = @( 5 | # {workspaceId}/{reportId} - get from the URL: https://app.powerbi.com/groups/8d820de8-53a6-4531-885d-20b27c85f413/reports/1c4595e4-b634-4f53-a963-df3a718f36ba 6 | "cdee92d2-3ff9-43e2-9f71-0916e888ad27/bd56fa8f-e9b4-490f-b2bf-53012e5a55b3" 7 | #,"aafc842b-f6d8-4006-bbdc-0eb2130b3fa6/1333a143-37b9-40df-a798-a26e77619c03" 8 | ) 9 | , $datasetId = "8659b030-2f7d-44bf-a265-5bf2425fb04f" 10 | ) 11 | 12 | 13 | $ErrorActionPreference = "Stop" 14 | $VerbosePreference = "Continue" 15 | 16 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 17 | 18 | Connect-PowerBIServiceAccount 19 | 20 | $reports |% { 21 | 22 | $report = $_ 23 | 24 | $workspaceId = Split-Path $_ -Parent 25 | $reportId = Split-Path $_ -Leaf 26 | 27 | Write-Host "Rebinding report '$workspaceId/$reportId' to dataset '$datasetId'" 28 | 29 | $bodyStr = @{datasetId = $datasetId} | ConvertTo-Json 30 | 31 | Invoke-PowerBIRestMethod -url "groups/$workspaceId/reports/$reportId/Rebind" -method Post -body $bodyStr -ErrorAction Continue 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Dataset-UpdateCredentials.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | $ErrorActionPreference = "Stop" 4 | 5 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 6 | 7 | $workspaceId = "9f19cb45-1182-4656-82b0-e7a049985835" 8 | $datasetId = "d688fbb7-01ba-4a8c-a9b6-5d1e262bb347" 9 | $username = "sqluser" 10 | $password = "sqluserpwd" 11 | 12 | Connect-PowerBIServiceAccount 13 | 14 | $datasources = @(Invoke-PowerBIRestMethod -url "groups/$workspaceId/datasets/$datasetId/datasources" -method Get | ConvertFrom-Json | Select -ExpandProperty value) 15 | 16 | $datasource = $datasources[0] 17 | 18 | $updateDatasourceBodyStr = "{ 19 | ""credentialDetails"": { 20 | ""credentials"": ""{\""credentialData\"":[{\""name\"":\""username\"",\""value\"":\""$username\""},{\""name\"":\""password\"",\""value\"":\""$password\""}]}"", 21 | ""credentialType"": ""Basic"", 22 | ""encryptedConnection"": ""NotEncrypted"", 23 | ""encryptionAlgorithm"": ""None"", 24 | ""privacyLevel"": ""None"", 25 | ""useCallerAADIdentity"": false 26 | } 27 | } 28 | " 29 | 30 | Invoke-PowerBIRestMethod -url "gateways/$($datasource.gatewayId)/datasources/$($datasource.datasourceId)" -method Patch -Body $updateDatasourceBodyStr 31 | -------------------------------------------------------------------------------- /PaginatedReport-Export.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | param( 4 | $workspaceId = "cdee92d2-3ff9-43e2-9f71-0916e888ad27" 5 | , 6 | $reportId = "041eca9c-f5ad-465f-8153-dc257d56c485" 7 | , 8 | $format = "XLSX" 9 | ) 10 | 11 | $ErrorActionPreference = "Stop" 12 | $VerbosePreference = "SilentlyContinue" 13 | 14 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 15 | 16 | 17 | Connect-PowerBIServiceAccount 18 | 19 | $bodyStr = @{format=$format} | ConvertTo-Json 20 | 21 | $result = Invoke-PowerBIRestMethod -url "groups/$workspaceId/reports/$reportId/ExportTo" -body $bodyStr -method Post 22 | 23 | $status = $result | ConvertFrom-Json 24 | 25 | while($status.status -in @("NotStarted", "Running")) 26 | { 27 | Write-Host "Sleeping..." 28 | 29 | Start-Sleep -Seconds 5 30 | 31 | $result = Invoke-PowerBIRestMethod -url "groups/$workspaceId/reports/$reportId/exports/$($status.id)" -method Get 32 | 33 | $status = $result | ConvertFrom-Json 34 | 35 | } 36 | 37 | if ($status.status -eq "Succeeded") 38 | { 39 | $result = Invoke-PowerBIRestMethod -url "groups/$workspaceId/reports/$reportId/exports/$($status.id)/file" -method Get -OutFile "$currentPath\output\export.$format" 40 | } -------------------------------------------------------------------------------- /Admin-GetGroupsAsAdmin.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1077" } 2 | 3 | param ( 4 | $workspaces = @("cdee92d2-3ff9-43e2-9f71-0916e888ad27"), 5 | $expand="users,reports,dashboards,datasets,dataflows,workbooks", 6 | $outputPath = ".\output\getgroupsadmin" 7 | ) 8 | 9 | $ErrorActionPreference = "Stop" 10 | 11 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 12 | 13 | Set-Location $currentPath 14 | 15 | New-Item -ItemType Directory -Path $outputPath -ErrorAction SilentlyContinue | Out-Null 16 | 17 | try { 18 | $token = Get-PowerBIAccessToken 19 | } 20 | catch { 21 | $pbiAccount = Connect-PowerBIServiceAccount 22 | } 23 | 24 | foreach ($workspace in $workspaces) 25 | { 26 | $apiUrl = "admin/groups/$workspace" 27 | 28 | if ($expand) 29 | { 30 | $apiUrl = $apiUrl + "?`$expand=$expand" 31 | } 32 | 33 | $workspacesScanRequests = Invoke-PowerBIRestMethod -Url $apiUrl -method Get | ConvertFrom-Json 34 | 35 | $outputFilePath = "$outputPath\$workspace.json" 36 | 37 | New-Item -Path (Split-Path $outputFilePath -Parent) -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null 38 | 39 | ConvertTo-Json $workspacesScanRequests -Depth 10 -Compress | Out-File $outputFilePath -force 40 | 41 | } -------------------------------------------------------------------------------- /Admin-O365AuditLog.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules ExchangeOnlineManagement 2 | 3 | param ( 4 | $numberDays = 1, 5 | $outputPath = ".\output\o365Audit" 6 | ) 7 | 8 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 9 | 10 | Set-Location $currentPath 11 | 12 | # Ensure its enabled: https://learn.microsoft.com/en-us/microsoft-365/compliance/audit-log-enable-disable?view=o365-worldwide 13 | # Get-AdminAuditLogConfig | FL UnifiedAuditLogIngestionEnabled 14 | 15 | Connect-ExchangeOnline 16 | 17 | # Now you can query for Power BI activity. In this example, the results are limited to 18 | 19 | $pivotDate = [datetime]::UtcNow.Date.AddDays(-1*$numberDays) 20 | 21 | while ($pivotDate -le [datetime]::UtcNow) { 22 | 23 | Write-Host "Getting audit data for: '$($pivotDate.ToString("yyyyMMdd"))'" 24 | 25 | $results = Search-UnifiedAuditLog -StartDate $pivotDate -EndDate $pivotDate.AddHours(24).AddSeconds(-1) -RecordType PowerBIAudit -ResultSize 5000 26 | 27 | $outputFilePath = "$outputPath\auditLogsO365\{0:yyyyMMdd}.json" -f $pivotDate 28 | 29 | New-Item -Path (Split-Path $outputFilePath -Parent) -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null 30 | 31 | ConvertTo-Json @($results) -Compress -Depth 10 | Out-File $outputFilePath -force 32 | 33 | $pivotDate = $pivotDate.AddDays(1) 34 | } -------------------------------------------------------------------------------- /Workspace-SearchAndDelete.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | # This script requires you to authenticate with a Power BI Admin account 4 | 5 | param( 6 | $searchPattern = @("*PBIDevOps*", "Test*") 7 | ) 8 | 9 | $ErrorActionPreference = "Stop" 10 | $VerbosePreference = "SilentlyContinue" 11 | 12 | Connect-PowerBIServiceAccount 13 | 14 | $workspaces = Get-PowerBIWorkspace -Scope Organization -All -Filter "tolower(state) eq 'active'" 15 | 16 | $filteredWorkspaces = @( 17 | $workspaces |? { 18 | 19 | $workspaceName = $_.Name 20 | 21 | $searchPattern |? { $workspaceName -like $_} 22 | 23 | } 24 | ) 25 | 26 | if ($filteredWorkspaces.Count -eq 0) 27 | { 28 | Write-Host "No active workspaces found where Workspace Name like $([string]::join(",", $searchPattern))" 29 | } 30 | else 31 | { 32 | # TODO - Its not possible to delete workspace as admin, but its possible to add yourself as admin and delete afterq 33 | 34 | Write-Host "Found '$($filteredWorkspaces.Count)' workspaces" 35 | 36 | $filteredWorkspaces | Format-Table 37 | 38 | $confirmation = Read-Host "Are you Sure You Want To Proceed (y)" 39 | 40 | if ($confirmation -ieq 'y') { 41 | 42 | foreach($workspace in $filteredWorkspaces) 43 | { 44 | Write-Host "Deleting Workspace: $($workspace.Name)" 45 | 46 | Invoke-PowerBIRestMethod -Method Delete -Url "groups/$($workspace.id)" 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /Dataset-ExecuteXMLAQuery.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | $serverName = "powerbi://api.powerbi.com/v1.0/myorg/WWI" 3 | , $datasetName = "WWI - Sales" 4 | , $username = "app:@" 5 | , $password = "" 6 | , $query = "EVALUATE Customer" 7 | ) 8 | 9 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 10 | 11 | Add-Type -Path "$currentPath\lib\Microsoft.AnalysisServices.AdomdClient.dll" 12 | 13 | $asModelType = [Microsoft.AnalysisServices.AdomdClient.AdomdConnection] 14 | 15 | $assembly = $asModelType.Assembly 16 | 17 | Write-Host "Assembly version loaded: '$($assembly.FullName)' from '$($assembly.Location)'" 18 | 19 | try 20 | { 21 | 22 | $conn = new-object Microsoft.AnalysisServices.AdomdClient.AdomdConnection 23 | 24 | $conn.ConnectionString = "Data Source=$serverName;User Id=$username;Password=$password" 25 | 26 | $conn.Open() 27 | 28 | $conn.ChangeDatabase($datasetName) 29 | 30 | $cmd = $conn.CreateCommand() 31 | 32 | $cmd.CommandText = $query 33 | 34 | $cmd.CommandTimeout = 30 35 | 36 | $reader = $cmd.ExecuteReader() 37 | 38 | $i = 0 39 | 40 | Write-Host "Reading..." 41 | 42 | while($reader.Read()) 43 | { 44 | if ($i % 50 -eq 0) 45 | { 46 | Write-Host "Reading... ($i)" 47 | } 48 | 49 | $i++ 50 | } 51 | 52 | $reader.Dispose() 53 | 54 | } 55 | finally 56 | { 57 | if ($conn) 58 | { 59 | Write-Host "Closing connection" 60 | 61 | $conn.Dispose() 62 | } 63 | } 64 | 65 | 66 | -------------------------------------------------------------------------------- /Dataset-GetRefreshHistory-Dataset.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | param( 4 | $datasets = @( 5 | @{workspaceId = "ff9f6b54-83e8-4aa5-901b-a7675e001c77";datasetId = "4aee5203-4d36-4b2e-87b4-904a7bd38016"} 6 | , 7 | @{workspaceId = "7331d174-e08f-4802-acba-898b8cecbc75";datasetId = "ecb5768c-3057-433a-91c0-c56bece634ae"} 8 | ) 9 | ) 10 | 11 | $ErrorActionPreference = "Stop" 12 | 13 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 14 | 15 | # Get running refreshes, only 1 operation is allowed "Only one refresh operation at a time is accepted for a dataset. If there's a current running refresh operation and another is submitted" 16 | 17 | Connect-PowerBIServiceAccount 18 | 19 | foreach ($dataset in $datasets) 20 | { 21 | Write-Host "Workspace: $($dataset.workspaceId); Dataset: $($dataset.datasetId)" 22 | 23 | Write-Host "Gateway Info" 24 | 25 | $bindedGateways = Invoke-PowerBIRestMethod -url "groups/$($dataset.workspaceId)/datasets/$($dataset.datasetId)/Default.GetBoundGatewayDatasources" -method Get | ConvertFrom-Json | select -ExpandProperty value 26 | 27 | $bindedGateways | Format-Table 28 | 29 | Write-Host "Refresh History" 30 | 31 | # https://docs.microsoft.com/en-us/rest/api/power-bi/datasets/get-refresh-history-in-group 32 | 33 | $refreshes = Invoke-PowerBIRestMethod -url "groups/$($dataset.workspaceId)/datasets/$($dataset.datasetId)/refreshes?`$top=5" -method Get | ConvertFrom-Json | select -ExpandProperty value 34 | 35 | $refreshes | Format-Table 36 | } 37 | 38 | -------------------------------------------------------------------------------- /Dataset-RefreshAsync-Cancel.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | param( 4 | $workspaceId = "cdee92d2-3ff9-43e2-9f71-0916e888ad27" 5 | , 6 | $datasetId = "ff63139f-eea9-4296-a5e4-3f56c0005701" 7 | ) 8 | 9 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 10 | 11 | Connect-PowerBIServiceAccount 12 | 13 | # Get running refreshes, only 1 operation is allowed "Only one refresh operation at a time is accepted for a dataset. If there's a current running refresh operation and another is submitted" 14 | 15 | Write-Host "Get Runnning Refreshes" 16 | 17 | $refreshes = Invoke-PowerBIRestMethod -url "groups/$workspaceId/datasets/$datasetId/refreshes?`$top=5" -method Get | ConvertFrom-Json | select -ExpandProperty value 18 | 19 | $refreshes = @($refreshes |? { $_.extendedStatus -in @("Unknown", "inProgress", "notStarted") }) 20 | 21 | Write-Host "Canceling '$($refreshes.Count)' refreshes" 22 | 23 | foreach($refresh in $refreshes) 24 | { 25 | $refreshId = $refresh.requestId 26 | 27 | do 28 | { 29 | Write-Host "Cancelling..." 30 | 31 | Invoke-PowerBIRestMethod -url "groups/$workspaceId/datasets/$datasetId/refreshes/$refreshId" -method Delete | ConvertFrom-Json 32 | 33 | $refreshDetails = Invoke-PowerBIRestMethod -url "groups/$workspaceId/datasets/$datasetId/refreshes/$refreshId" -method Get | ConvertFrom-Json 34 | 35 | Write-Host "Status: $($refreshDetails.status)" 36 | 37 | Write-Host "sleeping..." 38 | 39 | Start-Sleep -Seconds 2 40 | 41 | } 42 | while($refreshDetails.extendedStatus -iin @("notStarted", "Unknown", "inProgress")) 43 | 44 | } -------------------------------------------------------------------------------- /Dataset-QSO.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | param( 4 | $workspaceId = "70f9aed2-7b42-45dd-a1fe-94c6e9cc89c1" 5 | , 6 | $datasetId = "99b285a0-086e-47b7-8af6-7938b698e37a" 7 | ) 8 | 9 | try { Get-PowerBIAccessToken | out-null } catch { Connect-PowerBIServiceAccount } 10 | 11 | Write-Host "Checking sync status for dataset '$datasetId'" 12 | 13 | $syncStatus = Invoke-PowerBIRestMethod -Url "groups/$workspaceId/datasets/$datasetId/syncStatus" -Method Get | ConvertFrom-Json 14 | 15 | if ($syncStatus.commitVersion -ne $syncStatus.minActiveReadVersion) 16 | { 17 | Write-Warning "Dataset is out of sync, reason: $($syncStatus.triggerReason)" 18 | 19 | $syncStatus | Format-List 20 | 21 | Write-Host "Syncing" 22 | 23 | $syncStatus = Invoke-PowerBIRestMethod -Url "groups/$workspaceId/datasets/$datasetId/sync" -Body "" -Method Post | ConvertFrom-Json 24 | 25 | while ($syncStatus.commitVersion -ne $syncStatus.minActiveReadVersion) 26 | { 27 | $syncStatus = Invoke-PowerBIRestMethod -Url "groups/$workspaceId/datasets/$datasetId/syncStatus" -Method Get | ConvertFrom-Json 28 | 29 | if ($syncStatus.commitVersion -eq $syncStatus.minActiveReadVersion) 30 | { 31 | Write-Host "Dataset in Sync!" 32 | Write-Host "Duration: $(($syncStatus.syncEndTime - $syncStatus.syncStartTime).TotalSeconds)" 33 | Write-Host "Read Replica timestamp: $($syncStatus.minActiveReadTimestamp)" 34 | 35 | } 36 | else { 37 | Write-Warning "Dataset not in sync, sleeping..." 38 | Start-Sleep -Seconds 5 39 | } 40 | } 41 | 42 | } 43 | else { 44 | Write-Host "Dataset in Sync!" 45 | } 46 | 47 | -------------------------------------------------------------------------------- /XMLA - Partitions - Process (Manual).ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | $serverName = "powerbi://api.powerbi.com/v1.0/myorg/Session%20-%20PBI%20Dev%20on%20Steroids" 3 | , $databaseName = "WWI - Sales (Partitioned)" 4 | , $tables = @('Orders','Calendar') 5 | ) 6 | 7 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 8 | 9 | Import-Module "$currentPath\TOMHelper.psm1" -Force 10 | 11 | $asModelType = [Microsoft.AnalysisServices.Tabular.Model] 12 | 13 | $assembly = $asModelType.Assembly 14 | 15 | Write-Host "Assembly version loaded: '$($assembly.FullName)' from '$($assembly.Location)'" 16 | 17 | try 18 | { 19 | Write-Host "Connecting to Server: '$serverName'" 20 | 21 | $server = New-Object Microsoft.AnalysisServices.Tabular.Server 22 | 23 | $server.Connect($serverName) 24 | 25 | Write-Host "Connected" 26 | 27 | foreach($table in $tables) 28 | { 29 | $refreshCommand = @{ 30 | "refresh" = @{ 31 | "type" = "full" 32 | ; 33 | "objects"= @( 34 | @{ 35 | "database" = $databaseName 36 | ; 37 | "table" = $table 38 | } 39 | ) 40 | } 41 | } 42 | 43 | Write-Host "Refreshing table: '$table'" 44 | 45 | $script = $refreshCommand | ConvertTo-Json -Depth 5 46 | 47 | $sw = [system.diagnostics.stopwatch]::StartNew() 48 | 49 | $result = $server.Execute($script) 50 | 51 | $sw.Stop() 52 | 53 | Write-Host "Time: $($sw.Elapsed.TotalSeconds)s" 54 | 55 | } 56 | 57 | } 58 | finally 59 | { 60 | if($server) 61 | { 62 | $server.Dispose() 63 | } 64 | } 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /Report-ExportPowerBIReport.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | param( 4 | $workspaceId = "cdee92d2-3ff9-43e2-9f71-0916e888ad27" 5 | , 6 | $reportId = "d471e78e-2251-4085-9a60-678c0d4b5dfa" 7 | , 8 | $format = "PDF" 9 | ) 10 | 11 | $ErrorActionPreference = "Stop" 12 | $VerbosePreference = "SilentlyContinue" 13 | 14 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 15 | 16 | $appId = "" 17 | $tenantId = "" 18 | $appSecret = "" 19 | 20 | if ($appId) 21 | { 22 | $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $appId, ($appSecret | ConvertTo-SecureString -AsPlainText -Force) 23 | 24 | Connect-PowerBIServiceAccount -ServicePrincipal -Tenant $tenantId -Credential $credential 25 | } 26 | else { 27 | Connect-PowerBIServiceAccount 28 | } 29 | 30 | $bodyStr = @{format=$format} | ConvertTo-Json 31 | 32 | $result = Invoke-PowerBIRestMethod -url "groups/$workspaceId/reports/$reportId/ExportTo" -body $bodyStr -method Post 33 | 34 | $status = $result | ConvertFrom-Json 35 | 36 | while($status.status -in @("NotStarted", "Running")) 37 | { 38 | Write-Host "Sleeping..." 39 | 40 | Start-Sleep -Seconds 5 41 | 42 | $result = Invoke-PowerBIRestMethod -url "groups/$workspaceId/reports/$reportId/exports/$($status.id)" -method Get 43 | 44 | $status = $result | ConvertFrom-Json 45 | 46 | } 47 | 48 | if ($status.status -eq "Succeeded") 49 | { 50 | $outputFile = "$currentPath\output\export_$($status.id).$format" 51 | 52 | Write-Host "Export Successfull, writing output to: '$outputFile'" 53 | 54 | $result = Invoke-PowerBIRestMethod -url "groups/$workspaceId/reports/$reportId/exports/$($status.id)/file" -method Get -OutFile $outputFile 55 | } -------------------------------------------------------------------------------- /Admin-AddUserToWorkspace.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt.Profile"; ModuleVersion="1.2.1026" } 2 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt.Workspaces"; ModuleVersion="1.2.1026" } 3 | 4 | param( 5 | # See more examples here: https://learn.microsoft.com/en-us/rest/api/power-bi/admin/groups-add-user-as-admin 6 | $identity = "FreeUser@rrmsft.onmicrosoft.com", 7 | $identityType = "User", 8 | $workspaceRole = "Member", 9 | $workspaces = @("664e5e57-47a2-4cbc-9539-99da11abf341"), 10 | $servicePrincipalId = "", 11 | $servicePrincipalSecret = "", 12 | $servicePrincipalTenantId = "" 13 | ) 14 | 15 | $ErrorActionPreference = "Stop" 16 | $VerbosePreference = "SilentlyContinue" 17 | 18 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 19 | 20 | Set-Location $currentPath 21 | 22 | # Get token with admin account 23 | 24 | if ($servicePrincipalId) 25 | { 26 | $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $servicePrincipalId, ($servicePrincipalSecret | ConvertTo-SecureString -AsPlainText -Force) 27 | 28 | $pbiAccount = Connect-PowerBIServiceAccount -ServicePrincipal -Tenant $servicePrincipalTenantId -Credential $credential 29 | } 30 | else { 31 | $pbiAccount = Connect-PowerBIServiceAccount 32 | } 33 | 34 | Write-Host "Login with: $($pbiAccount.UserName)" 35 | 36 | Write-Host "Workspaces to set security: $($workspaces.Count)" 37 | 38 | foreach($workspace in $workspaces) 39 | { 40 | Write-Host "Adding identity to workspace: $workspace)" 41 | 42 | $body = @{ 43 | "identifier" = $identity 44 | ; 45 | "groupUserAccessRight" = $workspaceRole 46 | ; 47 | "principalType" = $identityType 48 | } 49 | 50 | $bodyStr = ($body | ConvertTo-Json) 51 | 52 | Invoke-PowerBIRestMethod -method Post -url "admin/groups/$workspace/users" -body $bodyStr 53 | } 54 | 55 | 56 | -------------------------------------------------------------------------------- /Dataset-ExecuteDAXQuery.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 4 | 5 | $outputPath = "$currentPath\output" 6 | 7 | $relativeDate = [datetime]"2016-01-22" 8 | 9 | $queries = @( 10 | @{ 11 | DatasetId = "0ed822d8-46c5-4132-8dfc-0b813e126e06" 12 | ; 13 | QueryName = "EmployeesWithLessSales" 14 | ; 15 | Query = "EVALUATE 16 | 17 | VAR p_currentDate = dt""$($relativeDate.ToString("yyyy-MM-dd"))"" 18 | 19 | return 20 | FILTER( 21 | SUMMARIZECOLUMNS( 22 | 'Employee'[Employee] 23 | , TREATAS({p_currentDate}, 'Calendar'[Date]) 24 | , ""Sales Amount"", [Sales Amount] 25 | , ""Sales Qty"", [Sales Qty] 26 | , ""Sales Profit"", [Sales Profit] 27 | , ""Sales Amount vs LY"", [% Sales Amount vs ly] 28 | ) 29 | , [Sales Amount vs LY] < 0)" 30 | } 31 | , 32 | @{ 33 | DatasetId = "0ed822d8-46c5-4132-8dfc-0b813e126e06" 34 | ; 35 | QueryName = "Employees" 36 | ; 37 | Query = "EVALUATE 'Employee'" 38 | } 39 | ) 40 | 41 | Connect-PowerBIServiceAccount 42 | 43 | foreach ($query in $queries) 44 | { 45 | Write-Host "Executing query: $($query.QueryName)" 46 | 47 | $body = @{ 48 | "queries" = @( 49 | @{ 50 | "query" = $query.Query 51 | 52 | ; 53 | "includeNulls" = $false 54 | } 55 | ) 56 | } 57 | 58 | $bodyStr = $body | ConvertTo-Json 59 | 60 | $result = Invoke-PowerBIRestMethod -url "datasets/$($query.DatasetId)/executeQueries" -body $bodyStr -method Post | ConvertFrom-Json 61 | 62 | $result.results[0].tables[0].rows | Format-Table 63 | 64 | $outputFile = ("$outputPath\{0:yyyyMMdd}\$($query.QueryName).csv" -f [datetime]::UtcNow) 65 | 66 | New-Item -ItemType Directory -Path (Split-Path $outputFile -Parent) -ErrorAction SilentlyContinue | Out-Null 67 | 68 | $result.results[0].tables[0].rows | ConvertTo-Csv -NoTypeInformation | Out-File $outputFile 69 | 70 | } 71 | 72 | 73 | -------------------------------------------------------------------------------- /Report-Rebind-Auto (Non Admin).ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | param( 4 | $oldDataSetId = "db12ea48-1bbd-4cb1-90bb-65897897a3a3" # Dataset B 5 | , 6 | $newDataSetId = "663ee438-1470-44a1-bc07-ce7c4b703760" # DataSet A 7 | ) 8 | 9 | cls 10 | 11 | $ErrorActionPreference = "Stop" 12 | $VerbosePreference = "SilentlyContinue" 13 | 14 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 15 | 16 | $relatedContentPath = "$currentPath\relatedcontent.json" 17 | 18 | if (!(Test-Path $relatedContentPath)) 19 | { 20 | Write-Warning "Execute a DataLineage on the dataset '$oldDataSetId' with a browser network trace and save the request 'datalineage/impactAnalysis/models/*/relatedcontent' to local file '$relatedContentPath'" 21 | return 22 | } 23 | 24 | $relatedContent = Get-Content $relatedContentPath | ConvertFrom-Json 25 | 26 | if (!$relatedContent.RelatedReports -or $relatedContent.RelatedReports.Count -eq 0) 27 | { 28 | Write-Warning "No reports to related to dataset '$oldDataSetId'" 29 | return 30 | } 31 | 32 | Connect-PowerBIServiceAccount 33 | 34 | foreach ($report in $relatedContent.RelatedReports) 35 | { 36 | $reportId = $report.ReportObjectId 37 | $workspaceId = $report.workspaceObjectId 38 | 39 | $bodyStr = @{datasetId = $newDataSetId} | ConvertTo-Json 40 | 41 | # If is a personal workspace, workspaceid must be null on rebind 42 | 43 | if ($report.WorkspaceType -eq 3) 44 | { 45 | $workspaceId = $null 46 | } 47 | 48 | Write-Host "Rebinding report '$workspaceId/$reportId' to new dataset '$newDataSetId'" 49 | 50 | if ($workspaceId) 51 | { 52 | $apiUrl = "groups/$workspaceId/reports/$reportId/Rebind" 53 | } 54 | else 55 | { 56 | $apiUrl = "reports/$reportId/Rebind" 57 | } 58 | 59 | Invoke-PowerBIRestMethod -Url $apiUrl -method Post -body $bodyStr -ErrorAction Continue 60 | } 61 | 62 | -------------------------------------------------------------------------------- /API-UserApis.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1077" } 2 | 3 | param ( 4 | $workspaces = @("401927c5-2c16-4d48-85c9-21f1038c7862"), 5 | $servicePrincipalId = "", 6 | $servicePrincipalSecret = "", 7 | $servicePrincipalTenantId = "" 8 | ) 9 | 10 | $ErrorActionPreference = "Stop" 11 | 12 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 13 | 14 | Set-Location $currentPath 15 | 16 | try { 17 | $token = Get-PowerBIAccessToken 18 | } 19 | catch { 20 | if ($servicePrincipalId) 21 | { 22 | $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $servicePrincipalId, ($servicePrincipalSecret | ConvertTo-SecureString -AsPlainText -Force) 23 | 24 | $pbiAccount = Connect-PowerBIServiceAccount -ServicePrincipal -Tenant $servicePrincipalTenantId -Credential $credential 25 | } 26 | else { 27 | $pbiAccount = Connect-PowerBIServiceAccount 28 | } 29 | } 30 | 31 | foreach($workspace in $workspaces) 32 | { 33 | Write-Host "Calling UserAPIs for workspace: $workspace" 34 | 35 | Write-Host "Workspace" 36 | 37 | Invoke-PowerBIRestMethod -Url "groups?`$filter=contains(id,'$workspace')" -method Get | ConvertFrom-Json | select -ExpandProperty value | Format-List 38 | 39 | Write-Host "Datasets" 40 | 41 | Invoke-PowerBIRestMethod -Url "groups/$workspace/datasets" -method Get | ConvertFrom-Json | select -ExpandProperty value | Format-List 42 | 43 | Write-Host "Reports" 44 | 45 | Invoke-PowerBIRestMethod -Url "groups/$workspace/reports" -method Get | ConvertFrom-Json | select -ExpandProperty value | Format-List 46 | 47 | Write-Host "Dashboards" 48 | 49 | Invoke-PowerBIRestMethod -Url "groups/$workspace/dashboards" -method Get | ConvertFrom-Json | select -ExpandProperty value | Format-List 50 | 51 | Write-Host "Dataflows" 52 | 53 | Invoke-PowerBIRestMethod -Url "groups/$workspace/dataflows" -method Get | ConvertFrom-Json | select -ExpandProperty value | Format-List 54 | } 55 | -------------------------------------------------------------------------------- /MicrosoftPowerBIMgmt-Demo.ps1: -------------------------------------------------------------------------------- 1 | cls 2 | 3 | Install-Module MicrosoftPowerBIMgmt 4 | 5 | # Authentication Prompt 6 | Connect-PowerBIServiceAccount 7 | 8 | #region Service Principal 9 | $appId = "" 10 | $tenantId = "" 11 | $appSecret = "" 12 | $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $appId, ($appSecret | ConvertTo-SecureString -AsPlainText -Force) 13 | #Disconnect-PowerBIServiceAccount 14 | Connect-PowerBIServiceAccount -ServicePrincipal -Tenant $tenantId -Credential $credential 15 | #endregion 16 | 17 | # Call the API using the PowerBIRestMethod 18 | $result = Invoke-PowerBIRestMethod -Url "groups" -Method Get | ConvertFrom-Json | select -ExpandProperty value 19 | $result | Format-Table 20 | $result.Count 21 | 22 | # RAW API Call - https://docs.microsoft.com/en-us/powershell/module/microsoftpowerbimgmt.profile/invoke-powerbirestmethod?view=powerbi-ps 23 | # https://app.powerbi.com/groups/1eb4ce83-58cb-4360-8ac5-b7930e81360a/list 24 | $result = Invoke-PowerBIRestMethod -Url "groups/da09fedd-9cb2-460a-986f-9624a1662168/datasets" -Method Get | ConvertFrom-Json | select -ExpandProperty value 25 | $result | Format-Table 26 | $result.Count 27 | 28 | # https://docs.microsoft.com/en-us/rest/api/power-bi/admin/apps-get-apps-as-admin 29 | $result = Invoke-PowerBIRestMethod -Url "admin/apps?`$top=2000" -Method Get | ConvertFrom-Json | select -ExpandProperty value 30 | $result | Format-Table 31 | $result.Count 32 | 33 | # https://docs.microsoft.com/en-us/powershell/module/microsoftpowerbimgmt.workspaces/get-powerbiworkspace?view=powerbi-ps 34 | $result = Get-PowerBIWorkspace -All 35 | $result | Format-Table 36 | $result.Count 37 | 38 | # Call API as Admin 39 | 40 | $result = Get-PowerBIWorkspace -Scope Organization -All 41 | $result | Format-Table 42 | $result.Count 43 | 44 | # https://docs.microsoft.com/en-us/powershell/module/microsoftpowerbimgmt.data/get-powerbidataset?view=powerbi-ps 45 | $result = Get-PowerBIDataset -WorkspaceId "1eb4ce83-58cb-4360-8ac5-b7930e81360a" 46 | $result 47 | $result.Count 48 | 49 | $result = Get-PowerBIDataset -WorkspaceId "8d820de8-53a6-4531-885d-20b27c85f413" 50 | $result 51 | $result.Count 52 | 53 | 54 | -------------------------------------------------------------------------------- /Report-Rebind-Auto (Admin).ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | # This script requires you to authenticate with a Power BI Admin account 4 | 5 | param( 6 | $oldDataSetId = "db12ea48-1bbd-4cb1-90bb-65897897a3a3" # Dataset B 7 | , 8 | $newDataSetId = "663ee438-1470-44a1-bc07-ce7c4b703760" # DataSet A 9 | ) 10 | 11 | $ErrorActionPreference = "Stop" 12 | $VerbosePreference = "SilentlyContinue" 13 | 14 | Connect-PowerBIServiceAccount 15 | 16 | $workspaces = Get-PowerBIWorkspace -Scope Organization -All -Include @("reports", "datasets") 17 | 18 | $reportDataSetRelationship = $workspaces |%{ 19 | $workspace = $_ 20 | 21 | $workspace.reports |% { 22 | 23 | $report = $_ 24 | 25 | Write-Output @{ 26 | workspaceId = $workspace.id 27 | ; 28 | workspaceType = $workspace.type 29 | ; 30 | reportId = $report.id 31 | ; 32 | datasetId = $report.datasetId 33 | } 34 | } 35 | } 36 | 37 | $oldDataSetRelatedReports = $reportDataSetRelationship |? datasetId -eq $oldDataSetId 38 | 39 | if ($oldDataSetRelatedReports.Count -eq 0) 40 | { 41 | Write-Warning "No reports connected to dataset '$oldDataSetId'" 42 | } 43 | 44 | foreach ($report in $oldDataSetRelatedReports) 45 | { 46 | $reportId = $report.ReportId 47 | $workspaceId = $report.WorkspaceId 48 | 49 | $bodyStr = @{datasetId = $newDataSetId} | ConvertTo-Json 50 | 51 | # If is a personal workspace, workspaceid must be null on rebind 52 | 53 | if ($report.WorkspaceType -eq "PersonalGroup") 54 | { 55 | $workspaceId = $null 56 | } 57 | 58 | Write-Host "Rebinding report '$workspaceId/$reportId' to new dataset '$newDataSetId'" 59 | 60 | if ($workspaceId) 61 | { 62 | $apiUrl = "groups/$workspaceId/reports/$reportId/Rebind" 63 | } 64 | else 65 | { 66 | $apiUrl = "reports/$reportId/Rebind" 67 | } 68 | 69 | Invoke-PowerBIRestMethod -Url $apiUrl -method Post -body $bodyStr -ErrorAction Continue 70 | } 71 | 72 | -------------------------------------------------------------------------------- /Report-Rebind-AllReportsFromWorkspaceToDataset.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | param( 4 | $workspaces = @( 5 | # Demo Pipelines - Sales Reports 6 | @{ 7 | workspaceId = 'aafc842b-f6d8-4006-bbdc-0eb2130b3fa6' 8 | ; datasetId = 'e0f9d017-7e56-45ec-b39c-d09352a64828' 9 | } 10 | , 11 | @{ 12 | workspaceId = 'e1a86102-084c-4cb3-b4d8-addf75d38e6c' 13 | ; datasetId = 'efd01a85-b741-4205-b0d3-afe8beea9ce8' 14 | } 15 | , 16 | @{ 17 | workspaceId = '35314361-6641-4c5b-a6e6-91bb10c60ed0' 18 | ; datasetId = 'a94e51e4-e49a-4878-b223-ae3d8bd4b205' 19 | } 20 | , 21 | # Demo Pipelines - Marketing Reports 22 | @{ 23 | workspaceId = '54dd21eb-a543-461b-b947-03ff40db93fd' 24 | ; datasetId = 'e0f9d017-7e56-45ec-b39c-d09352a64828' 25 | } 26 | , 27 | @{ 28 | workspaceId = 'da58456c-45c7-418a-8e89-bbbb3c1618ac' 29 | ; datasetId = 'efd01a85-b741-4205-b0d3-afe8beea9ce8' 30 | } 31 | , 32 | @{ 33 | workspaceId = 'e6386d06-8368-49da-a796-e47fb8b23fd4' 34 | ; datasetId = 'a94e51e4-e49a-4878-b223-ae3d8bd4b205' 35 | } 36 | ) 37 | ) 38 | 39 | 40 | $ErrorActionPreference = "Stop" 41 | $VerbosePreference = "Continue" 42 | 43 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 44 | 45 | Connect-PowerBIServiceAccount 46 | 47 | foreach($workspace in $workspaces) 48 | { 49 | $workspaceId = $workspace.workspaceId 50 | $datasetId = $workspace.datasetId 51 | 52 | $reports = Get-PowerBIReport -WorkspaceId $workspaceId 53 | 54 | foreach ($report in $reports) 55 | { 56 | 57 | $reportId = $report.Id 58 | 59 | Write-Host "Rebinding report '$workspaceId/$reportId' to dataset '$datasetId'" 60 | 61 | $bodyStr = @{datasetId = $datasetId} | ConvertTo-Json 62 | 63 | Invoke-PowerBIRestMethod -url "groups/$workspaceId/reports/$reportId/Rebind" -method Post -body $bodyStr -ErrorAction Continue 64 | 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Workspace-Premium Capacity Switch.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | param ( 4 | $sourceCapacityName = "rrpbiembedded" 5 | , 6 | $targetCapacityName = "rrmsft_P1" 7 | ) 8 | 9 | $ErrorActionPreference = "Stop" 10 | 11 | Connect-PowerBIServiceAccount 12 | 13 | Write-Host "Getting existent capacities" 14 | 15 | $capacities = Get-PowerBICapacity 16 | 17 | $capacities | Format-Table 18 | 19 | $sourceCapacity = ($capacities |? DisplayName -eq $sourceCapacityName | select -First 1) 20 | 21 | if (!$sourceCapacity) 22 | { 23 | throw "Cannot find capacity with name '$sourceCapacityName'" 24 | } 25 | 26 | $targetCapacity = ($capacities |? DisplayName -eq $targetCapacityName | select -First 1) 27 | 28 | if (!$targetCapacity) 29 | { 30 | throw "Cannot find capacity with name '$targetCapacityName'" 31 | } 32 | 33 | Write-Host "Getting workspaces" 34 | 35 | $premiumWorkspaces = Get-PowerBIWorkspace -Scope Organization -All -Filter "isOnDedicatedCapacity eq true and tolower(state) eq 'active'" 36 | 37 | $sourcePremiumWorkspaces = @($premiumWorkspaces |? {$_.capacityId -eq $sourceCapacity.Id}) | sort-object -Property Id -Unique 38 | 39 | if ($sourcePremiumWorkspaces.Count -gt 0) 40 | { 41 | Write-Host "Assigning $($sourcePremiumWorkspaces.Count) workspaces to new capacity '$($targetCapacity.DisplayName)' / '$($targetCapacity.Id)'" 42 | 43 | $sourcePremiumWorkspaces | Format-Table 44 | 45 | $confirmation = Read-Host "Are you Sure You Want To Proceed (y)" 46 | 47 | if ($confirmation -ieq 'y') { 48 | 49 | $workspaceIds = @($sourcePremiumWorkspaces.id) 50 | 51 | # Unassign workspaces 52 | 53 | $body = @{ 54 | capacityMigrationAssignments= @(@{ 55 | targetCapacityObjectId = $targetCapacity.Id; 56 | workspacesToAssign = $workspaceIds 57 | }) 58 | } 59 | 60 | $bodyStr = ConvertTo-Json $body -Depth 3 61 | 62 | Invoke-PowerBIRestMethod -url "admin/capacities/AssignWorkspaces" -method Post -body $bodyStr 63 | } 64 | } 65 | else 66 | { 67 | Write-Host "No workspaces on source capacity: '$($sourceCapacity.DisplayName)' / '$($sourceCapacity.Id)'" 68 | } -------------------------------------------------------------------------------- /XMLA - Partitions - Process (Unprocessed).ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | $serverName = "powerbi://api.powerbi.com/v1.0/myorg/Session%20-%20PBI%20on%20Steroids" 3 | , $databaseName = "WWI - Sales (Partitioned)" 4 | , $maxParallelism = 2 5 | , $batchPartitionCount = 5 6 | ) 7 | 8 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 9 | 10 | Import-Module "$currentPath\TOMHelper.psm1" -Force 11 | 12 | $tables = Get-ASTable -serverName $serverName -databaseName $databaseName -includePartitions 13 | 14 | $sw = [system.diagnostics.stopwatch]::StartNew() 15 | 16 | foreach($table in $tables) 17 | { 18 | try 19 | { 20 | Write-Host "Processing table: $($table.Name)" 21 | 22 | $unprocessedPartitions = @($table.Partitions |? State -ne "Ready") 23 | 24 | if ($unprocessedPartitions.Count -gt 0) 25 | { 26 | $batchSkip = 0 27 | 28 | $processOrders = @() 29 | 30 | do 31 | { 32 | $batchPartitions = @($unprocessedPartitions | select -First $batchPartitionCount -Skip $batchSkip) 33 | 34 | if ($batchPartitions.Count -gt 0) 35 | { 36 | $processOrders += @{ 37 | Server = $serverName 38 | ; 39 | DatabaseName = $databaseName 40 | ; 41 | TableName = $table.Name 42 | ; 43 | Partitions = @($batchPartitions.Name) 44 | ; 45 | Group = "Group $batchSkip" 46 | } 47 | 48 | $batchSkip += $batchPartitions.Count 49 | } 50 | } 51 | while($batchPartitions) 52 | 53 | $results = Invoke-ASTableProcess -tables $processOrders -maxParallelism $maxParallelism 54 | } 55 | else 56 | { 57 | Write-Host "Table fully processed" 58 | } 59 | } 60 | catch 61 | { 62 | Write-Error -Message "Error on table '$($table.Name)'; Error: '$($_.Exception.Message)'" -Exception $_.Exception -ErrorAction Continue 63 | } 64 | } 65 | 66 | $sw.Stop() 67 | 68 | Write-Host "Time: $($sw.Elapsed.TotalSeconds)s" 69 | 70 | -------------------------------------------------------------------------------- /Admin-WorkspaceScan.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1077" } 2 | 3 | param ( 4 | $workspaces = @("cdee92d2-3ff9-43e2-9f71-0916e888ad27"), 5 | $getInfoDetails = "getArtifactUsers=true&lineage=true&datasourceDetails=true&datasetSchema=true&datasetExpressions=true", 6 | $outputPath = ".\output\workspacesinglescan" 7 | ) 8 | 9 | $ErrorActionPreference = "Stop" 10 | 11 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 12 | 13 | Set-Location $currentPath 14 | 15 | $scansOutputPath = $outputPath 16 | 17 | New-Item -ItemType Directory -Path $scansOutputPath -ErrorAction SilentlyContinue | Out-Null 18 | 19 | try { 20 | $token = Get-PowerBIAccessToken 21 | } 22 | catch { 23 | $pbiAccount = Connect-PowerBIServiceAccount 24 | } 25 | 26 | $bodyStr = @{"workspaces" = @($workspaces) } | ConvertTo-Json 27 | 28 | # $script: scope to reference the outerscope variable 29 | 30 | $workspacesScanRequests = @(Invoke-PowerBIRestMethod -Url "admin/workspaces/getInfo?$getInfoDetails" -Body $bodyStr -method Post | ConvertFrom-Json) 31 | 32 | while(@($workspacesScanRequests |? status -in @("Running", "NotStarted"))) 33 | { 34 | Write-Host "Waiting for scan results, sleeping..." 35 | 36 | Start-Sleep -Seconds 5 37 | 38 | foreach ($workspaceScanRequest in $workspacesScanRequests) 39 | { 40 | $scanStatus = Invoke-PowerBIRestMethod -Url "admin/workspaces/scanStatus/$($workspaceScanRequest.id)" -method Get | ConvertFrom-Json 41 | 42 | Write-Host "Scan '$($scanStatus.id)' : '$($scanStatus.status)'" 43 | 44 | $workspaceScanRequest.status = $scanStatus.status 45 | } 46 | } 47 | 48 | foreach ($workspaceScanRequest in $workspacesScanRequests) 49 | { 50 | $scanResult = Invoke-PowerBIRestMethod -Url "admin/workspaces/scanResult/$($workspaceScanRequest.id)" -method Get | ConvertFrom-Json 51 | 52 | Write-Host "Scan Result'$($scanStatus.id)' : '$($scanResult.workspaces.Count)'" 53 | 54 | $outputFilePath = ("$scansOutputPath\{0:yyyy}{0:MM}{0:dd}_$($workspaceScanRequest.id).json" -f [datetime]::Today) 55 | 56 | $scanResult | Add-Member –MemberType NoteProperty –Name "scanCreatedDateTime" –Value $workspaceScanRequest.createdDateTime -Force 57 | 58 | ConvertTo-Json $scanResult -Depth 10 -Compress | Out-File $outputFilePath -force 59 | } 60 | -------------------------------------------------------------------------------- /Dataset-RefreshAsync.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | param( 4 | $workspaceId = "6b75551a-6107-4688-a1bf-f6a8eb52df71" 5 | , 6 | $datasetId = "d6ba1cf6-ca85-4676-9db5-fa91c3c69f19" 7 | , 8 | $type = "full" 9 | , 10 | $maxParallelism = 6 11 | , 12 | $commitMode = "transactional" 13 | #$commitMode = "partialBatch" 14 | , 15 | $retryCount = 1 16 | , 17 | $objects = @( 18 | 19 | ) 20 | 21 | ) 22 | 23 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 24 | 25 | $executeJsonObj = @{ 26 | "type" = $type 27 | ; 28 | "commitMode" = $commitMode 29 | ; 30 | "maxParallelism" = $maxParallelism 31 | ; 32 | "retryCount" = $retryCount 33 | ; 34 | "objects" = $objects 35 | } 36 | 37 | $executeJsonBody = $executeJsonObj | ConvertTo-Json -Depth 5 38 | 39 | Connect-PowerBIServiceAccount 40 | 41 | # Get running refreshes, only 1 operation is allowed "Only one refresh operation at a time is accepted for a dataset. If there's a current running refresh operation and another is submitted" 42 | 43 | $refreshes = Invoke-PowerBIRestMethod -url "groups/$workspaceId/datasets/$datasetId/refreshes?`$top=10" -method Get | ConvertFrom-Json | select -ExpandProperty value 44 | 45 | if (!($refreshes |? { $_.refreshType -eq "ViaEnhancedApi" -and $_.status -iin @("Unknown", "inProgress", "notStarted") })) 46 | { 47 | Write-Host "Posting a new Refresh Command" 48 | 49 | Invoke-PowerBIRestMethod -url "groups/$workspaceId/datasets/$datasetId/refreshes" -method Post -Body $executeJsonBody 50 | } 51 | 52 | Write-Host "Waiting for refresh to end" 53 | 54 | $refreshes = Invoke-PowerBIRestMethod -url "groups/$workspaceId/datasets/$datasetId/refreshes?`$top=10" -method Get | ConvertFrom-Json | select -ExpandProperty value 55 | 56 | $refreshId = $refreshes[0].requestId 57 | 58 | do 59 | { 60 | $refreshDetails = Invoke-PowerBIRestMethod -url "groups/$workspaceId/datasets/$datasetId/refreshes/$refreshId" -method Get | ConvertFrom-Json 61 | 62 | Write-Host "Status: $($refreshDetails.status)" 63 | Write-Host ($refreshDetails.objects | format-table | out-string) 64 | Write-Host "sleeping..." 65 | 66 | Start-Sleep -Seconds 2 67 | 68 | } 69 | while($refreshDetails.status -iin @("Unknown", "inProgress", "notStarted")) 70 | 71 | Write-Host "Refresh complete: $((([datetime]$refreshDetails.endTime) - ([datetime]$refreshDetails.startTime)).TotalSeconds)s" 72 | 73 | $refreshDetails | Format-Table 74 | 75 | $refreshDetails.objects | Format-Table -------------------------------------------------------------------------------- /Security - EnsureUserOnAllWorkspaces.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt.Profile"; ModuleVersion="1.2.1026" } 2 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt.Workspaces"; ModuleVersion="1.2.1026" } 3 | 4 | param( 5 | # See more examples here: https://learn.microsoft.com/en-us/rest/api/power-bi/admin/groups-add-user-as-admin 6 | $identity = "5922c898-2076-4695-a4a3-953e8a62c1a7", 7 | $identityType = "App", 8 | # $identity = "user@company.com", 9 | # $identityType = "User", 10 | $workspaceRole = "Member", 11 | $workspaceFilter = @() # @("28544d33-de5f-49cf-8e45-a2a8784fe31f", "d0641e3b-a0c6-404b-a422-afe7be6b2a4f") 12 | ) 13 | 14 | $ErrorActionPreference = "Stop" 15 | $VerbosePreference = "SilentlyContinue" 16 | 17 | try 18 | { 19 | $stopwatch = [System.Diagnostics.Stopwatch]::new() 20 | $stopwatch.Start() 21 | 22 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 23 | 24 | Set-Location $currentPath 25 | 26 | # Get token with admin account 27 | 28 | Connect-PowerBIServiceAccount 29 | 30 | # Get all tenant workspaces 31 | 32 | $workspaces = Get-PowerBIWorkspace -Scope Organization -All 33 | 34 | Write-Host "Workspaces: $($workspaces.Count)" 35 | 36 | # Only look at active workspaces and V2 37 | 38 | $workspaces = @($workspaces |? {$_.type -eq "Workspace" -and $_.state -eq "Active"}) 39 | 40 | if ($workspaceFilter -and $workspaceFilter.Count -gt 0) 41 | { 42 | $workspaces = @($workspaces |? { $workspaceFilter -contains $_.Id}) 43 | } 44 | 45 | # Filter workspaces where the serviceprincipal is not there 46 | 47 | $workspaces = $workspaces |? { 48 | 49 | $members = @($_.users |? { $_.identifier -eq $identity }) 50 | 51 | if ($members.Count -eq 0) 52 | { 53 | $true 54 | } 55 | else 56 | { 57 | $false 58 | } 59 | } 60 | 61 | Write-Host "Workspaces to set security: $($workspaces.Count)" 62 | 63 | foreach($workspace in $workspaces) 64 | { 65 | Write-Host "Adding service principal to workspace: $($workspace.name) ($($workspace.id))" 66 | 67 | $body = @{ 68 | "identifier" = $identity 69 | ; 70 | "groupUserAccessRight" = $workspaceRole 71 | ; 72 | "principalType" = $identityType 73 | } 74 | 75 | $bodyStr = ($body | ConvertTo-Json) 76 | 77 | Invoke-PowerBIRestMethod -method Post -url "admin/groups/$($workspace.id)/users" -body $bodyStr 78 | } 79 | 80 | } 81 | finally 82 | { 83 | $stopwatch.Stop() 84 | 85 | Write-Host "Ellapsed: $($stopwatch.Elapsed.TotalSeconds)s" 86 | } 87 | -------------------------------------------------------------------------------- /Report-PublishReport.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | param( 4 | [string]$path = ".\SampleReport - Live.pbix", 5 | [string]$workspaceId = "00fad993-e80b-45b1-9376-0a1e637cafa9", 6 | [string]$datasetId, 7 | [bool]$handleReportDuplication = $true 8 | ) 9 | 10 | $ErrorActionPreference = "Stop" 11 | 12 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 13 | 14 | Set-Location $currentPath 15 | 16 | Connect-PowerBIServiceAccount 17 | 18 | $reports = Get-ChildItem -File -Path $path -ErrorAction SilentlyContinue 19 | 20 | foreach($pbixFile in $reports) 21 | { 22 | Write-Host "Deploying report: '$($pbixFile.Name)'" 23 | 24 | $filePath = $pbixFile.FullName 25 | 26 | $reportName = [System.IO.Path]::GetFileNameWithoutExtension($filePath) 27 | 28 | $targetReport = @(Get-PowerBIReport -WorkspaceId $workspaceId -Name $reportName) 29 | 30 | if ($targetReport.Count -eq 0 -or !$handleReportDuplication) 31 | { 32 | Write-Host "Uploading new report to workspace '$workspaceId'" 33 | 34 | $importResult = New-PowerBIReport -Path $filePath -WorkspaceId $workspaceId -Name $reportName -ConflictAction CreateOrOverwrite 35 | 36 | $targetReportId = $importResult.Id 37 | } 38 | elseif ($handleReportDuplication) 39 | { 40 | if ($targetReport.Count -gt 1) 41 | { 42 | throw "More than one report with name '$reportName', please remove and keep only one" 43 | } 44 | 45 | Write-Host "Report already exists on workspace '$workspaceId', uploading to temp report & updatereportcontent" 46 | 47 | $targetReport = $targetReport[0] 48 | 49 | $targetReportId = $targetReport.id 50 | 51 | # Upload a temp report and update the report content of the target report 52 | 53 | # README - This is required because of a limitation of Import API that always duplicate the report if the dataset is different (may be solved in the future) 54 | 55 | $tempReportName = "Temp_$([System.Guid]::NewGuid().ToString("N"))" 56 | 57 | Write-Host "Uploadind as a temp report '$tempReportName'" 58 | 59 | $importResult = New-PowerBIReport -Path $filePath -WorkspaceId $workspaceId -Name $tempReportName -ConflictAction Abort 60 | 61 | $tempReportId = $importResult.Id 62 | 63 | Write-Host "Updating report content" 64 | 65 | $updateContentResult = Invoke-PowerBIRestMethod -method Post -Url "groups/$workspaceId/reports/$targetReportId/UpdateReportContent" -Body (@{ 66 | sourceType = "ExistingReport" 67 | sourceReport = @{ 68 | sourceReportId = $tempReportId 69 | sourceWorkspaceId = $workspaceId 70 | } 71 | } | ConvertTo-Json) 72 | 73 | # Delete the temp report 74 | 75 | Write-Host "Deleting temp report '$tempReportId'" 76 | 77 | Invoke-PowerBIRestMethod -Method Delete -Url "groups/$workspaceId/reports/$tempReportId" 78 | } 79 | 80 | if ($targetReportId -and $dataSetId) 81 | { 82 | Write-Host "Rebinding to dataset '$dataSetId'" 83 | 84 | Invoke-PowerBIRestMethod -Method Post -Url "groups/$workspaceId/reports/$targetReportId/Rebind" -Body "{datasetId: '$dataSetId'}" | Out-Null 85 | } 86 | 87 | } -------------------------------------------------------------------------------- /Admin-ActivityScan.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1077" } 2 | 3 | param ( 4 | $numberDays = 2, 5 | $outputPath = ".\output\activity", 6 | $filter = "", 7 | $outputBatchCount = 5000, 8 | $servicePrincipalId = "", 9 | $servicePrincipalSecret = "", 10 | $servicePrincipalTenantId = "" 11 | ) 12 | 13 | $ErrorActionPreference = "Stop" 14 | 15 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 16 | 17 | Set-Location $currentPath 18 | 19 | try { 20 | $token = Get-PowerBIAccessToken 21 | } 22 | catch { 23 | if ($servicePrincipalId) 24 | { 25 | $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $servicePrincipalId, ($servicePrincipalSecret | ConvertTo-SecureString -AsPlainText -Force) 26 | 27 | $pbiAccount = Connect-PowerBIServiceAccount -ServicePrincipal -Tenant $servicePrincipalTenantId -Credential $credential 28 | } 29 | else { 30 | $pbiAccount = Connect-PowerBIServiceAccount 31 | } 32 | } 33 | 34 | Write-Host "Login with: $($pbiAccount.UserName)" 35 | 36 | $maxHistoryDate = [datetime]::UtcNow.Date.AddDays(-30) 37 | 38 | $pivotDate = [datetime]::UtcNow.Date.AddDays(-1*$numberDays) 39 | 40 | Write-Host "Since: $($pivotDate.ToString("s"))" 41 | 42 | while ($pivotDate -le [datetime]::UtcNow) { 43 | 44 | Write-Host "Getting audit data for: '$($pivotDate.ToString("yyyyMMdd"))'" 45 | 46 | $activityAPIUrl = "admin/activityevents?startDateTime='$($pivotDate.ToString("s"))'&endDateTime='$($pivotDate.AddHours(24).AddSeconds(-1).ToString("s"))'" 47 | 48 | if ($filter) 49 | { 50 | $activityAPIUrl += "&`$filter=$filter" 51 | } 52 | 53 | $audits = @() 54 | $pageIndex = 1 55 | $flagNoActivity = $true 56 | 57 | do 58 | { 59 | if (!$result.continuationUri) 60 | { 61 | $result = Invoke-PowerBIRestMethod -Url $activityAPIUrl -method Get | ConvertFrom-Json 62 | } 63 | else { 64 | $result = Invoke-PowerBIRestMethod -Url $result.continuationUri -method Get | ConvertFrom-Json 65 | } 66 | 67 | if ($result.activityEventEntities) 68 | { 69 | $audits += @($result.activityEventEntities) 70 | } 71 | 72 | if ($audits.Count -ne 0 -and ($audits.Count -ge $outputBatchCount -or $result.continuationToken -eq $null)) 73 | { 74 | # To avoid duplicate data on existing files, first dont append pageindex to overwrite existing full file 75 | 76 | if ($pageIndex -eq 1) 77 | { 78 | $outputFilePath = ("$outputPath\{0:yyyyMMdd}.json" -f $pivotDate) 79 | } 80 | else { 81 | $outputFilePath = ("$outputPath\{0:yyyyMMdd}_$pageIndex.json" -f $pivotDate) 82 | } 83 | 84 | Write-Host "Writing '$($audits.Count)' audits" 85 | 86 | New-Item -Path (Split-Path $outputFilePath -Parent) -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null 87 | 88 | ConvertTo-Json @($audits) -Compress -Depth 10 | Out-File $outputFilePath -force 89 | 90 | $flagNoActivity = $false 91 | 92 | $pageIndex++ 93 | 94 | $audits = @() 95 | } 96 | } 97 | while($result.continuationToken -ne $null) 98 | 99 | if ($flagNoActivity) 100 | { 101 | Write-Warning "No audit logs for date: '$($pivotDate.ToString("yyyyMMdd"))'" 102 | } 103 | 104 | $pivotDate = $pivotDate.AddDays(1) 105 | } 106 | 107 | -------------------------------------------------------------------------------- /XMLA - Model - Replicate.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | $sourceWorkspace = "powerbi://api.powerbi.com/v1.0/myorg/Demo%20-%20XMLA%20Replicate%20Dataset" 3 | , 4 | $targetWorkspaces = @( 5 | "powerbi://api.powerbi.com/v1.0/myorg/Demo%20-%20XMLA%20Replicate%20Dataset%201" 6 | , 7 | "powerbi://api.powerbi.com/v1.0/myorg/Demo%20-%20XMLA%20Replicate%20Dataset%202" 8 | , 9 | "powerbi://api.powerbi.com/v1.0/myorg/Demo%20-%20XMLA%20Replicate%20Dataset%203" 10 | ) 11 | , 12 | $databaseName = "WWI-Model" 13 | ) 14 | 15 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 16 | 17 | Set-Location $currentPath 18 | 19 | Import-Module "$currentPath\TOMHelper.psm1" -Force 20 | 21 | $bimFilePath = "$currentPath\output\$databaseName.bim" 22 | 23 | New-Item -ItemType Directory -Path (Split-Path $bimFilePath -Parent) -ErrorAction SilentlyContinue | Out-Null 24 | 25 | # Get the bim file from the workspace 26 | 27 | Get-ASDatabase -serverName $sourceWorkspace -databaseName $databaseName -bimFilePath $bimFilePath 28 | 29 | foreach ($targetWorkspace in $targetWorkspaces) 30 | { 31 | Write-Host "Deploying to workspace '$targetWorkspace'" 32 | 33 | $bimJson = Get-Content $bimFilePath | ConvertFrom-Json 34 | 35 | # For simplicity, this should be a configuration 36 | 37 | if ($targetWorkspace.EndsWith("1")) 38 | { 39 | $states = @("Texas", "Pennsylvania") 40 | } 41 | elseif($targetWorkspace.EndsWith("2")) 42 | { 43 | $states = @("Florida", "New York") 44 | } 45 | else 46 | { 47 | $states = @("California", "Colorado") 48 | } 49 | 50 | $statesFilterStr = ($states |% {"'$_'"}) -join "," 51 | 52 | $partitions = @( 53 | @{ 54 | TableName = "Sales" 55 | ; 56 | Partitions = @( 57 | @{ 58 | Name = "Sales" 59 | ; 60 | Type = "M" 61 | ; 62 | Query = "let 63 | Source = Sql.Database(Server, Database, [Query="" 64 | SELECT 65 | [Sale Key] 66 | , s.[City Key] 67 | ,[Customer Key] 68 | ,[Bill To Customer Key] 69 | ,[Stock Item Key] 70 | ,[Invoice Date Key] 71 | , case when (ABS(CAST(CAST(NEWID() AS VARBINARY) AS INT)) % 10) > 5 then 72 | DATEADD(DAY, ABS(CAST(CAST(NEWID() AS VARBINARY) AS INT)) % 300, [Invoice Date Key]) else 73 | null end 74 | [Invoice Paid Date Key] 75 | ,[Delivery Date Key] 76 | , s.[Salesperson Key] [Employee Key] 77 | ,[Quantity] 78 | ,[Unit Price] 79 | ,[Total Excluding Tax] [Total Amount] 80 | ,[Tax Amount] 81 | ,[Profit] 82 | FROM [Fact].[Sale] s 83 | left join [Dimension].[City] c on c.[City Key] = s.[City Key] 84 | where c.[State Province] in ($statesFilterStr)""]) 85 | in 86 | Source" 87 | } 88 | ) 89 | } 90 | ) 91 | 92 | # Change the partition in the local BIM file 93 | 94 | Add-ASTablePartition -bimFilePath $bimFilePath -partitions $partitions -removeDefaultPartition -Verbose 95 | 96 | # Deploy/Update the database in the target workspace 97 | 98 | Update-ASDatabase -serverName $targetWorkspace -databaseName $databaseName -bimFilePath $bimFilePath -deployPartitions:$true -deployRoles:$false -deployConnections:$false 99 | 100 | # Process/Refresh the dataset 101 | 102 | Invoke-ASTableProcess -tables @(@{Server = $targetWorkspace; DatabaseName = $databaseName}) -maxParallelism 6 103 | } 104 | 105 | -------------------------------------------------------------------------------- /Realtime - Streaming Dataset.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | cls 4 | 5 | $VerbosePreference = "Continue" 6 | $ErrorActionPreference = "Stop" 7 | 8 | # Get the current folder of the running script 9 | 10 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 11 | 12 | $workspaceId = "833acf64-db3c-48bf-818e-6f5f998e4734" 13 | $datasetOperatorsName = "StreamingDataset - Calls - Operators" 14 | $datasetTotalsName = "StreamingDataset - Calls - Totals" 15 | 16 | # README - If the dataset is recreated these endpoints must be updated with the new API Url. You can get this URL on the Dataset settings 17 | $datasetOperatorsEndpoint = "https://api.powerbi.com/beta/3a364c67-349a-484d-8dcb-873c88970c00/datasets/2d5649fb-d841-4b2b-b7fd-d39e3998e76a/rows?key=RR3MP307efubWI9DiC%2FUjoXspeMjda%2B9p%2BWB8EM755tuPl7h2ZgEnx4CWsszxubozDdALTZSAGzjkd7MQZzcgQ%3D%3D" 18 | $datasetTotalsEndpoint = "https://api.powerbi.com/beta/3a364c67-349a-484d-8dcb-873c88970c00/datasets/56a56cfa-3210-4289-824c-a1728d16fb2f/rows?key=M8uWKTpjmPFN4UicLhdKWsl3jKjRFfhNcVwr%2BZnNqIBBhx7PUQEKGoDolYuxVAE5Eip4UpdFM0RpBRhjaA6LkQ%3D%3D" 19 | 20 | Connect-PowerBIServiceAccount 21 | 22 | $streamingDatasets = @( 23 | @{ 24 | name = $datasetOperatorsName 25 | ; defaultMode = "Streaming" 26 | ; tables = @( 27 | @{ name = $datasetOperatorsName 28 | ; columns = @( 29 | @{ name = "Timestamp"; dataType = "DateTime" } 30 | , @{ name = "Operator"; dataType = "String" } 31 | , @{ name = "WaitingTime"; dataType = "Int64" } 32 | ) 33 | } 34 | ) 35 | } 36 | ,@{ 37 | name = $datasetTotalsName 38 | ; defaultMode = "Streaming" 39 | ; tables = @( 40 | @{ name = $datasetTotalsName 41 | ; columns = @( 42 | @{ name = "Timestamp"; dataType = "DateTime" } 43 | , @{ name = "WaitingCalls"; dataType = "Int64" } 44 | , @{ name = "AnsweringCalls"; dataType = "Int64" } 45 | , @{ name = "TotalAnsweringCalls"; dataType = "Int64" } 46 | , @{ name = "TotalAnsweringCallsTarget"; dataType = "Int64" } 47 | ) 48 | } 49 | ) 50 | } 51 | ) 52 | 53 | foreach($streamingDataset in $streamingDatasets) 54 | { 55 | $dataset = Get-PowerBIDataset -WorkspaceId $workspaceId |? Name -eq $streamingDataset.name 56 | 57 | if (!$dataSet) 58 | { 59 | $bodyStr = $streamingDataset | ConvertTo-Json -Depth 5 60 | 61 | $result = Invoke-PowerBIRestMethod -method Post -url "groups/$workspaceId/datasets" -body $bodyStr 62 | } 63 | else 64 | { 65 | Write-Host "Dataset '$($dataset.name)' already created" 66 | } 67 | } 68 | 69 | Write-Host "Pushing Data" 70 | 71 | $totalCalls = 0 72 | 73 | while($true) 74 | { 75 | 76 | Write-Host "Pushing to 'CallCenter.Calls.Totals'" 77 | 78 | $totalCalls += (Get-Random -Minimum 1 -Maximum 10) 79 | 80 | $payload = @{ 81 | Timestamp = get-date -Format "u" 82 | WaitingCalls = (Get-Random -Minimum 0 -Maximum 10) 83 | AnsweringCalls =(Get-Random -Minimum 0 -Maximum 50) 84 | TotalAnsweringCalls = $totalCalls 85 | TotalAnsweringCallsTarget = 400 86 | } 87 | 88 | Invoke-RestMethod -Method Post -Uri $datasetTotalsEndpoint -Body (ConvertTo-Json @($payload)) 89 | 90 | Write-Output "Pushing to 'CallCenter.Calls'" 91 | 92 | $operators = @("Rui Romano", "Rui Quintino", "Bruno Ferreira", "Joana Barbosa", "Jose Barbosa", "Ricardo Santos", "Rui Barbosa", "Ricardo Calejo") 93 | 94 | $timestamp = get-date -Format "u" 95 | 96 | $payload = $operators | get-random -Count (Get-Random -Minimum 1 -Maximum 3) |%{ 97 | 98 | $waitOperator = $_ 99 | 100 | @{ 101 | Timestamp = $timestamp 102 | Operator = $waitOperator 103 | WaitingTime = (Get-Random -Minimum 60 -Maximum 360) 104 | } 105 | } 106 | 107 | Invoke-RestMethod -Method Post -Uri $datasetOperatorsEndpoint -Body (ConvertTo-Json @($payload)) 108 | 109 | Write-Output "Sleeping..." 110 | 111 | Start-Sleep -Seconds 1 112 | } 113 | 114 | -------------------------------------------------------------------------------- /API-ListSensitivityLabels.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1077" } 2 | 3 | param ( 4 | $graphUrl = "https://graph.microsoft.com/beta", 5 | $apiResource = "https://graph.microsoft.com", 6 | $servicePrincipalId = "", 7 | $servicePrincipalSecret = "", 8 | $servicePrincipalTenantId = "" 9 | 10 | ) 11 | 12 | $ErrorActionPreference = "Stop" 13 | 14 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 15 | 16 | Set-Location $currentPath 17 | 18 | function Get-AuthToken { 19 | [cmdletbinding()] 20 | param 21 | ( 22 | [string] 23 | $authority = "https://login.microsoftonline.com", 24 | [string] 25 | $tenantid, 26 | [string] 27 | $appid, 28 | [string] 29 | $appsecret , 30 | [string] 31 | $resource 32 | ) 33 | 34 | write-verbose "getting authentication token" 35 | 36 | $granttype = "client_credentials" 37 | 38 | $tokenuri = "$authority/$tenantid/oauth2/token?api-version=1.0" 39 | 40 | $appsecret = [System.Web.HttpUtility]::urlencode($appsecret) 41 | 42 | $body = "grant_type=$granttype&client_id=$appid&resource=$resource&client_secret=$appsecret" 43 | 44 | $token = invoke-restmethod -method post -uri $tokenuri -body $body 45 | 46 | $accesstoken = $token.access_token 47 | 48 | write-output $accesstoken 49 | 50 | } 51 | 52 | function Read-FromGraphAPI { 53 | [CmdletBinding()] 54 | param 55 | ( 56 | [string] 57 | $url, 58 | [string] 59 | $accessToken, 60 | [string] 61 | $format = "JSON" 62 | ) 63 | 64 | #https://blogs.msdn.microsoft.com/exchangedev/2017/04/07/throttling-coming-to-outlook-api-and-microsoft-graph/ 65 | 66 | try { 67 | $headers = @{ 68 | 'Content-Type' = "application/json" 69 | 'Authorization' = "Bearer $accessToken" 70 | } 71 | 72 | $result = Invoke-RestMethod -Method Get -Uri $url -Headers $headers 73 | 74 | if ($format -eq "CSV") { 75 | ConvertFrom-CSV -InputObject $result | Write-Output 76 | } 77 | else { 78 | Write-Output $result.value 79 | 80 | while ($result.'@odata.nextLink') { 81 | $result = Invoke-RestMethod -Method Get -Uri $result.'@odata.nextLink' -Headers $headers 82 | 83 | Write-Output $result.value 84 | } 85 | } 86 | 87 | } 88 | catch [System.Net.WebException] { 89 | $ex = $_.Exception 90 | 91 | try { 92 | $statusCode = $ex.Response.StatusCode 93 | 94 | if ($statusCode -eq 429) { 95 | $message = "429 Throthling Error - Sleeping..." 96 | 97 | Write-Host $message 98 | 99 | Start-Sleep -Seconds 1000 100 | } 101 | else { 102 | if ($ex.Response -ne $null) { 103 | $statusCode = $ex.Response.StatusCode 104 | 105 | $stream = $ex.Response.GetResponseStream() 106 | 107 | $reader = New-Object System.IO.StreamReader($stream) 108 | 109 | $reader.BaseStream.Position = 0 110 | 111 | $reader.DiscardBufferedData() 112 | 113 | $errorContent = $reader.ReadToEnd() 114 | 115 | $message = "$($ex.Message) - '$errorContent'" 116 | 117 | } 118 | else { 119 | $message = "$($ex.Message) - 'Empty'" 120 | } 121 | } 122 | 123 | Write-Error -Exception $ex -Message $message 124 | } 125 | finally { 126 | if ($reader) { $reader.Dispose() } 127 | 128 | if ($stream) { $stream.Dispose() } 129 | } 130 | } 131 | } 132 | 133 | $authToken = Get-AuthToken -resource $apiResource -appid $servicePrincipalId -appsecret $servicePrincipalSecret -tenantid $servicePrincipalTenantId 134 | 135 | $data = Read-FromGraphAPI -accessToken $authToken -url "$graphUrl/security/informationProtection/sensitivityLabels" | select * -ExcludeProperty "@odata.id" 136 | 137 | $data | Format-Table -------------------------------------------------------------------------------- /Dataset-DeployDatasetGTWBind.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules MicrosoftPowerBIMgmt 2 | 3 | $ErrorActionPreference = "Stop" 4 | 5 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 6 | 7 | $workspaceId = "114ee10f-5e0c-4821-bd5f-058eb8e1f287" 8 | $templateDatasetPath = "$currentPath\SampleFolderGTW.pbix" 9 | $datasetParams = $null 10 | #$datasetParams = @{"Server"=".\sql2019";"Database"="Contoso 1M";"TopN"="100000"} 11 | $numberDatasets = 20 12 | $gatewayName = "PBICATGTW1" 13 | $datasourceName = "temp folder" 14 | $refreshDatasets = $false 15 | $configureDatasets = $true 16 | $scheduleRefreshConfig = @{ 17 | "value" = @{ 18 | "enabled" = "true" 19 | ; 20 | "days" = @( "Friday") 21 | ; 22 | "times" = @("15:30") 23 | ; 24 | "localTimeZoneId" = "UTC" 25 | } 26 | } 27 | 28 | try { 29 | $token = Get-PowerBIAccessToken 30 | } 31 | catch { 32 | Connect-PowerBIServiceAccount 33 | } 34 | 35 | $gateways = Invoke-PowerBIRestMethod -url "gateways" -method Get | ConvertFrom-Json | Select -ExpandProperty value 36 | 37 | $gateway = $gateways |? name -eq $gatewayName 38 | 39 | if (!$gateway) 40 | { 41 | throw "Cannot find gateway '$gatewayName'" 42 | } 43 | 44 | $datasources = Invoke-PowerBIRestMethod -url "gateways/$($gateway.id)/datasources" -method Get | ConvertFrom-Json | Select -ExpandProperty value 45 | 46 | $datasource = $datasources |? datasourceName -eq $datasourceName 47 | 48 | if (!$datasource) 49 | { 50 | throw "Cannot find datasource '$datasourceName'" 51 | } 52 | 53 | $fileName = [System.IO.Path]::GetFileNameWithoutExtension($templateDatasetPath) 54 | 55 | $datasets = Get-PowerBIDataset -WorkspaceId $workspaceId 56 | 57 | $deployedDatasetNames = @() 58 | 59 | foreach($dsNumber in @(1..$numberDatasets)) 60 | { 61 | $datasetName = "$fileName - $("{0:000}" -f $dsNumber)" 62 | 63 | $deployedDatasetNames += $datasetName 64 | 65 | if (@($datasets |? Name -eq $datasetName)) 66 | { 67 | Write-Host "Dataset '$datasetName' already exists" 68 | } 69 | else 70 | { 71 | Write-Host "Deploying dataset '$datasetName'" 72 | $importResult = New-PowerBIReport -Path $templateDatasetPath -WorkspaceId $workspaceId -Name $datasetName -ConflictAction CreateOrOverwrite 73 | } 74 | 75 | } 76 | 77 | # Refresh with new deployed datasets 78 | 79 | $datasets = Get-PowerBIDataset -WorkspaceId $workspaceId 80 | 81 | $datasets = $datasets |? { 82 | $deployedDatasetNames -contains $_.name 83 | } 84 | 85 | Write-Host "Processing $($datasets.count) datasets" 86 | 87 | if ($configureDatasets) 88 | { 89 | $scheduleRefreshConfigStr = ConvertTo-Json $scheduleRefreshConfig -Depth 10 90 | 91 | foreach($dataset in $datasets) 92 | { 93 | Write-host "Configuring dataset '$($dataset.Name)'" 94 | 95 | if ($datasetParams || $datasetParams.Keys.Count -gt 0) 96 | { 97 | Write-Host "Set parameters for dataset" 98 | 99 | $parametersBody = @{updateDetails=@($datasetParams.Keys) |% { 100 | @{ 101 | "name" = $_ 102 | ; 103 | "newValue" = $datasetParams[$_] 104 | } 105 | } 106 | } 107 | 108 | $parametersBodyStr = ConvertTo-Json $parametersBody -Depth 10 109 | 110 | Invoke-PowerBIRestMethod -Url "groups/$workspaceId/datasets/$($dataset.id)/UpdateParameters" -Body $parametersBodyStr -Method Post | Out-Null 111 | } 112 | 113 | Write-Host "Bind dataset to Gateway" 114 | 115 | $bodyStr = @{gatewayObjectId = $datasource.gatewayId; datasourceObjectIds = @($datasource.id)} | ConvertTo-Json 116 | 117 | Invoke-PowerBIRestMethod -Url "groups/$workspaceId/datasets/$($dataset.Id)/Default.BindToGateway" -Method Post -Body $bodyStr | Out-Null 118 | 119 | Write-Host "Schedule refresh config on dataset" 120 | 121 | Invoke-PowerBIRestMethod -Url "groups/$workspaceId/datasets/$($dataset.Id)/refreshSchedule" -Method Patch -Body $scheduleRefreshConfigStr | Out-Null 122 | } 123 | } 124 | 125 | if ($refreshDatasets) 126 | { 127 | foreach($dataset in $datasets) 128 | { 129 | Write-Host "Refresh dataset '$($dataset.Name)'" 130 | 131 | Invoke-PowerBIRestMethod -Url "groups/$workspaceId/datasets/$($dataset.Id)/refreshes" -Method Post -Body "" | Out-Null 132 | } 133 | } -------------------------------------------------------------------------------- /Realtime - Push Dataset.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | cls 4 | 5 | $ErrorActionPreference = "Stop" 6 | $VerbosePreference = "Continue" 7 | 8 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 9 | 10 | # parameters 11 | 12 | # https://app.powerbi.com/groups/8e953ff5-38fa-4b9e-9e63-1b897f2f005b/dashboards/172c4f21-b41a-42eb-8356-e616bec80fcd?ctid=3a364c67-349a-484d-8dcb-873c88970c00 13 | 14 | $workspaceId = "3cdb0636-64c4-4ce7-b441-3f42b81ef01b" 15 | $datasetName = "PushDataSet - Server Counters" 16 | $reset = $true 17 | $computers = @($env:COMPUTERNAME) 18 | 19 | # Get the authentication token using ADAL library (OAuth) 20 | 21 | Connect-PowerBIServiceAccount 22 | 23 | # Dataset Schema 24 | 25 | $dataSetSchema = @{name = $datasetName 26 | ; defaultMode = "Push" 27 | ; tables = @( 28 | @{name = "Counters" 29 | ; columns = @( 30 | @{ name = "ComputerName"; dataType = "String"; isHidden = "true" } 31 | , @{ name = "TimeStamp"; dataType = "DateTime" } 32 | , @{ name = "CounterSet"; dataType = "String" } 33 | , @{ name = "CounterName"; dataType = "String" } 34 | , @{ name = "CounterValue"; dataType = "Double" } 35 | ) 36 | } 37 | , 38 | @{name = "Computers" 39 | ; columns = @( 40 | @{ name = "ComputerName"; dataType = "String" } 41 | , @{ name = "Domain"; dataType = "string" } 42 | , @{ name = "Manufacturer"; dataType = "string" } 43 | ) 44 | ;measures = @( 45 | @{name="Average CPU"; expression="CALCULATE(AVERAGE('Counters'[CounterValue]) / 100, FILTER('Counters', 'Counters'[CounterSet] = ""Processor(_Total)"" && 'Counters'[CounterName] = ""% Processor Time""))"; formatString="0.00%"} 46 | ) 47 | } 48 | ) 49 | ; relationships = @( 50 | @{ 51 | name = [guid]::NewGuid().ToString() 52 | ; fromTable = "Computers" 53 | ; fromColumn = "ComputerName" 54 | ; toTable = "Counters" 55 | ; toColumn = "ComputerName" 56 | ; crossFilteringBehavior = "oneDirection" 57 | 58 | }) 59 | } 60 | 61 | 62 | $dataset = Get-PowerBIDataset -WorkspaceId $workspaceId |? Name -eq $datasetName 63 | 64 | if (!$dataset) 65 | { 66 | Write-Host "Creating dataset" 67 | 68 | $bodyStr = $dataSetSchema | ConvertTo-Json -Depth 5 69 | 70 | $result = Invoke-PowerBIRestMethod -method Post -url "groups/$workspaceId/datasets?defaultRetentionPolicy=basicFIFO" -body $bodyStr 71 | 72 | $dataset = $result | ConvertFrom-Json 73 | 74 | Write-Verbose "DataSet created with id: '$($result.id)" 75 | } 76 | else 77 | { 78 | Write-Host "Dataset already created" 79 | } 80 | 81 | $datasetId = $dataset.Id 82 | 83 | if ($reset) 84 | { 85 | Write-Host "Clearing table rows" 86 | 87 | @("Counters","Computers") |% { 88 | $tableName = $_ 89 | Invoke-PowerBIRestMethod -method Delete -url "groups/$workspaceId/datasets/$datasetId/tables/$tableName/rows" 90 | } 91 | } 92 | 93 | # Push Data 94 | 95 | # Get Computer Info 96 | 97 | Write-Host "Writing Computers data" 98 | 99 | $computersInfo = $computers |% { 100 | 101 | $computerInfo = Get-WmiObject -Class Win32_ComputerSystem -ComputerName $_ 102 | 103 | Write-Output @{ 104 | ComputerName = $_ 105 | ; Domain = $computerInfo.Domain 106 | ; Manufacturer = $computerInfo.Manufacturer 107 | } 108 | } 109 | 110 | $bodyStr = @{rows = @($computersInfo)} | ConvertTo-Json 111 | 112 | Invoke-PowerBIRestMethod -method Post -url "groups/$workspaceId/datasets/$datasetId/tables/Computers/rows" -body $bodyStr | Out-Null 113 | 114 | # Collect data from continuosly in intervals of 5 seconds 115 | 116 | Write-Host "Writing Counter Data" 117 | 118 | $counters = Get-Counter -ComputerName $computers -ListSet @("processor", "memory", "physicaldisk") 119 | 120 | $counters | Get-Counter -Continuous -SampleInterval 5 |%{ 121 | 122 | # Parse the Counters into the schema of the PowerBI dataset 123 | 124 | $pbiData = $_.CounterSamples | Select @{Name = "ComputerName"; Expression = {$_.Path.Split('\')[2]}} ` 125 | , @{Name="TimeStamp"; Expression = {$_.TimeStamp.ToString("yyyy-MM-dd HH:mm:ss")}} ` 126 | , @{Name="CounterSet"; Expression = {$_.Path.Split('\')[3]}} ` 127 | , @{Name="CounterName"; Expression = {$_.Path.Split('\')[4]}} ` 128 | , @{Name="CounterValue"; Expression = {$_.CookedValue}} 129 | 130 | 131 | $bodyStr = @{rows = @($pbiData)} | ConvertTo-Json 132 | 133 | Invoke-PowerBIRestMethod -method Post -url "groups/$workspaceId/datasets/$datasetId/tables/Counters/rows" -body $bodyStr | Out-Null 134 | 135 | Write-Output "Sleeping..." 136 | } 137 | 138 | 139 | -------------------------------------------------------------------------------- /Admin-RefreshHistory.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1077" } 2 | 3 | param ( 4 | $outputPath = ".\output\refreshhistory", 5 | $servicePrincipalId = "", 6 | $servicePrincipalSecret = "", 7 | $servicePrincipalTenantId = "" 8 | ) 9 | 10 | $ErrorActionPreference = "Stop" 11 | 12 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 13 | 14 | Set-Location $currentPath 15 | 16 | try { 17 | $token = Get-PowerBIAccessToken 18 | } 19 | catch { 20 | if ($servicePrincipalId) 21 | { 22 | $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $servicePrincipalId, ($servicePrincipalSecret | ConvertTo-SecureString -AsPlainText -Force) 23 | 24 | $pbiAccount = Connect-PowerBIServiceAccount -ServicePrincipal -Tenant $servicePrincipalTenantId -Credential $credential 25 | } 26 | else { 27 | $pbiAccount = Connect-PowerBIServiceAccount 28 | } 29 | } 30 | 31 | New-Item -ItemType Directory -Path $outputPath -ErrorAction SilentlyContinue | Out-Null 32 | 33 | # Find Token Object Id, by decoding OAUTH TOken - https://blog.kloud.com.au/2019/07/31/jwtdetails-powershell-module-for-decoding-jwt-access-tokens-with-readable-token-expiry-time/ 34 | $token = (Get-PowerBIAccessToken -AsString).Split(" ")[1] 35 | $tokenPayload = $token.Split(".")[1].Replace('-', '+').Replace('_', '/') 36 | while ($tokenPayload.Length % 4) { $tokenPayload += "=" } 37 | $tokenPayload = [System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String($tokenPayload)) | ConvertFrom-Json 38 | # Service Principal 39 | #$pbiUserIdentifier = $tokenPayload.oid 40 | # PBI Account 41 | $pbiUserIdentifier = $tokenPayload.upn 42 | 43 | # Using the Admin API to get all workspaces with workspaces 44 | 45 | $workspaces = Get-PowerBIWorkspace -Scope Organization -All -Include Datasets 46 | 47 | Write-Host "Workspaces: $($workspaces.Count)" 48 | 49 | $workspaces = $workspaces |? { $_.users |? { $_.identifier -ieq $pbiUserIdentifier } } 50 | 51 | Write-Host "Workspaces where user is a member: $($workspaces.Count)" 52 | 53 | # Only look at Active, V2 Workspaces and with Datasets 54 | 55 | $workspaces = @($workspaces |? {$_.type -eq "Workspace" -and $_.state -eq "Active" -and $_.datasets.Count -gt 0}) 56 | 57 | if ($workspaceFilter -and $workspaceFilter.Count -gt 0) 58 | { 59 | $workspaces = @($workspaces |? { $workspaceFilter -contains $_.Id}) 60 | } 61 | 62 | Write-Host "Workspaces to get refresh history: $($workspaces.Count)" 63 | 64 | $total = $Workspaces.Count 65 | $item = 0 66 | 67 | foreach($workspace in $Workspaces) 68 | { 69 | $item++ 70 | 71 | Write-Host "Processing workspace: '$($workspace.Name)' $item/$total" 72 | 73 | Write-Host "Datasets: $(@($workspace.datasets).Count)" 74 | 75 | $refreshableDatasets = @($workspace.datasets |? { $_.isRefreshable -eq $true -and $_.addRowsAPIEnabled -eq $false}) 76 | 77 | Write-Host "Refreshable Datasets: $($refreshableDatasets.Count)" 78 | 79 | foreach($dataset in $refreshableDatasets) 80 | { 81 | try 82 | { 83 | Write-Host "Processing dataset: '$($dataset.name)'" 84 | 85 | Write-Host "Getting refresh history" 86 | 87 | $dsRefreshHistory = Invoke-PowerBIRestMethod -Url "groups/$($workspace.id)/datasets/$($dataset.id)/refreshes" -Method Get | ConvertFrom-Json 88 | 89 | $dsRefreshHistory = $dsRefreshHistory.value 90 | 91 | if ($dsRefreshHistory) 92 | { 93 | $dsRefreshHistory = @($dsRefreshHistory | Select *, @{Name="dataSetId"; Expression={ $dataset.id.ToString() }}, @{Name="dataSet"; Expression={ $dataset.name }}` 94 | ,@{Name="workspaceId"; Expression={ $workspace.id.ToString() }}, @{Name="workspace"; Expression={ $workspace.name }}, @{Name="configuredBy"; Expression={ $dataset.configuredBy }}) 95 | 96 | $dsRefreshHistoryGlobal += $dsRefreshHistory 97 | } 98 | } 99 | catch 100 | { 101 | $ex = $_.Exception 102 | 103 | Write-Error -message "Error processing dataset: '$($ex.Message)'" -ErrorAction Continue 104 | 105 | # If its unauthorized no need to advance to other datasets in this workspace 106 | 107 | if ($ex.Message.Contains("Unauthorized") -or $ex.Message.Contains("(404) Not Found")) 108 | { 109 | Write-Host "Got unauthorized/notfound, skipping workspace" 110 | 111 | break 112 | 113 | } 114 | } 115 | } 116 | } 117 | 118 | if ($dsRefreshHistoryGlobal.Count -gt 0) 119 | { 120 | $outputFilePath = "$outputPath\$(("{0:yyyy}{0:MM}{0:dd}" -f [datetime]::Today)).json" 121 | 122 | ConvertTo-Json @($dsRefreshHistoryGlobal) -Compress -Depth 5 | Out-File $outputFilePath -force 123 | } -------------------------------------------------------------------------------- /XMLA - ExecuteQuery.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | $connStr = "Provider=MSOLAP;Integrated Security=SSPI;Persist Security Info=False;Data Source=localhost\sql2019; Initial Catalog = AdventureWorks;SubQueries=2;" 3 | , 4 | $query = " 5 | SELECT NON EMPTY CrossJoin(Hierarchize(CrossJoin({[Geography].[Country Region Name].[Country Region Name].AllMembers}, {([Customer].[Education].[Education].AllMembers)})), {[Measures].[Internet Total Sales],[Measures].[Internet Total Units],[Measures].[Internet Total Tax Amt],[Measures].[Internet Total Product Cost],[Measures].[Internet Total Margin],[Measures].[Internet Total Freight],[Measures].[Internet Distinct Count Sales Order],[Measures].[Internet Total Discount Amount]}) DIMENSION PROPERTIES MEMBER_CAPTION ON COLUMNS , NON EMPTY Hierarchize(CrossJoin({[Customer].[Customer Id].[Customer Id].AllMembers}, {([Date].[Date].[Date].AllMembers,[Geography].[Postal Code].[Postal Code].AllMembers,[Internet Sales].[Due Date].[Due Date].AllMembers,[Product].[Product Id].[Product Id].AllMembers)})) DIMENSION PROPERTIES MEMBER_CAPTION ON ROWS FROM [Adventure Works Internet Sales Model] 6 | " 7 | , 8 | $logRowCount = 10000 9 | , 10 | $executionTimes = 2 11 | , 12 | $executionSleep = 5 13 | , 14 | $reuseConnection = $true 15 | , 16 | $assemblyPath = $null 17 | #$assemblyPath = "C:\Program Files\Microsoft Power BI Desktop\bin\Microsoft.PowerBI.AdomdClient.dll" 18 | #$assemblyPath = "C:\Program Files\On-premises data gateway\Microsoft.AnalysisServices.AdomdClient.dll" 19 | #$assemblyPath = "C:\Program Files\On-premises data gateway\m\Microsoft.PowerBI.AdomdClient.dll" 20 | ) 21 | 22 | cls 23 | 24 | $ErrorActionPreference = "Stop" 25 | 26 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 27 | 28 | if (!$assemblyPath) 29 | { 30 | $nugetName = "Microsoft.AnalysisServices.AdomdClient.retail.amd64" 31 | $nugetVersion = "19.54.1" 32 | 33 | if (!(Test-Path "$currentPath\Nuget\$nugetName.$nugetVersion" -PathType Container)) { 34 | Install-Package -Name $nugetName -ProviderName NuGet -Scope CurrentUser -RequiredVersion $nugetVersion -SkipDependencies -Destination "$currentPath\Nuget" -Force 35 | } 36 | 37 | $assemblyPath = Resolve-Path "$currentPath\Nuget\$nugetName.$nugetVersion\lib\net45\Microsoft.AnalysisServices.AdomdClient.dll" 38 | } 39 | 40 | Add-Type -Path $assemblyPath 41 | 42 | $assembly = [Microsoft.AnalysisServices.AdomdClient.AdomdConnection].Assembly 43 | 44 | Write-Host "Assembly version loaded: '$($assembly.FullName)' from '$($assembly.Location)'" 45 | 46 | $report = @() 47 | 48 | $conn = $null 49 | 50 | for ($i = 1; $i -le $executionTimes; $i++) 51 | { 52 | Write-Host "Execution $i / $executionTimes" 53 | 54 | try 55 | { 56 | if (!$reuseConnection -or !$conn) 57 | { 58 | Write-Host "Opening connection" 59 | 60 | $conn = new-object Microsoft.AnalysisServices.AdomdClient.AdomdConnection 61 | 62 | $conn.ConnectionString = $connStr 63 | 64 | $conn.Open() 65 | } 66 | 67 | try 68 | { 69 | 70 | Write-Host "Executing Query" 71 | 72 | $sw2 = $null 73 | 74 | $sw = New-Object System.Diagnostics.Stopwatch 75 | 76 | $sw.Start() 77 | 78 | $cmd = $conn.CreateCommand() 79 | 80 | $cmd.CommandText = $query 81 | 82 | $reader = $cmd.ExecuteReader() 83 | 84 | $rowCount = 0 85 | 86 | while($reader.Read()) 87 | { 88 | if (!$sw2) 89 | { 90 | $sw2 = New-Object System.Diagnostics.Stopwatch 91 | } 92 | 93 | if ($rowCount % $logRowCount -eq 0) 94 | { 95 | Write-Host "Reading... [$rowCount] - [$([datetime]::UtcNow.ToString("yyyy-MM-dd HH:mm:ss"))] - $([Math]::Round($sw2.Elapsed.TotalMilliseconds,0))ms" 96 | 97 | $sw2.Restart() 98 | } 99 | 100 | $rowCount++ 101 | } 102 | 103 | } 104 | finally 105 | { 106 | $sw.Stop() 107 | 108 | Write-Host "Query Execution: $($sw.Elapsed.TotalSeconds.ToString("N3"))s" 109 | 110 | $report += @{ExecutionId = $i; Duration = [Math]::Round($sw.Elapsed.TotalSeconds,3)} 111 | 112 | if ($reader) 113 | { 114 | $reader.Dispose() 115 | } 116 | 117 | if ($cmd) 118 | { 119 | $cmd.Dispose() 120 | } 121 | } 122 | } 123 | finally 124 | { 125 | if (!$reuseConnection -and $conn -ne $null) 126 | { 127 | Write-Host "Disposing Connection" 128 | 129 | $conn.Dispose() 130 | 131 | $conn = $null 132 | } 133 | } 134 | 135 | Write-Host "Sleeping..." 136 | 137 | Start-Sleep -Seconds $executionSleep 138 | } 139 | 140 | if ($conn -ne $null) 141 | { 142 | Write-Host "Disposing Connection" 143 | 144 | $conn.Dispose() 145 | 146 | $conn = $null 147 | } 148 | 149 | $report |% { [PSCustomObject]$_ } | format-table -AutoSize -------------------------------------------------------------------------------- /Hack - FixReportConnections.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } -Assembly System.IO.Compression 2 | 3 | <# 4 | WARNING - This script will change the internal files of your PBIX files. A backup will be made but its not supported by Microsoft. 5 | #> 6 | 7 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 8 | 9 | $reportsPath = "C:\Users\ruiromano\OneDrive - Microsoft\Work\202110\MMeyersSamples\Reports\Sales*" 10 | $datasetId = "80cc1541-7ba7-4a52-b7f2-496f58389975" 11 | $workingDir = "$currentPath\_temp\fixconnections" 12 | $backupdir = "$currentPath\_temp\fixconnections\bkup" 13 | $sharedDatasetsPath = "$currentPath\shareddatasets.json" 14 | 15 | if (!(Test-Path $sharedDatasetsPath)) 16 | { 17 | Write-Warning "Cannot find shareddatasets file '$sharedDatasetsPath'. Login to app.powerbi.com and execute a networktrace and save the 'sharedatasets' request to file: '$sharedDatasetsPath'." 18 | return 19 | } 20 | 21 | # Ensure folders exists 22 | @($workingDir, $backupDir) |% { New-Item -ItemType Directory -Path $_ -Force -ErrorAction SilentlyContinue | Out-Null } 23 | 24 | $reports = Get-ChildItem -File -Path "$reportsPath" -Include "*.pbix" -Recurse -ErrorAction SilentlyContinue 25 | 26 | if ($reports.Count -eq 0) 27 | { 28 | Write-Host "No reports on path '$reportsPath'" 29 | return 30 | } 31 | 32 | # Connect to PBI 33 | 34 | Connect-PowerBIServiceAccount 35 | 36 | $sharedDataSetsStr = Get-Content $sharedDatasetsPath 37 | #ConverFrom-Json doesnt like properties with same name 38 | $sharedDataSetsStr = $sharedDataSetsStr.Replace("nextRefreshTime","nextRefreshTime_").Replace("lastRefreshTime","lastRefreshTime_") 39 | $sharedDataSets = $sharedDataSetsStr | ConvertFrom-Json 40 | 41 | $reports |% { 42 | 43 | $pbixFile = $_ 44 | 45 | Write-Host "Fixing connection of report: '$($pbixFile.Name)'" 46 | 47 | $filePath = $pbixFile.FullName 48 | 49 | $fileName = [System.IO.Path]::GetFileName($pbixFile.FullName) 50 | 51 | $fileNameWithoutExt = [System.IO.Path]::GetFileNameWithoutExtension($fileName) 52 | 53 | Write-Host "Finding dataset model id of dataset '$dataSetId'" 54 | 55 | # Find model for the dataset 56 | 57 | $model = $sharedDataSets |? { $_.model.dbName -eq $dataSetId } 58 | 59 | if (!$model) 60 | { 61 | Write-Host "Cannot find a Power BI model for dataset '$dataSetId'" 62 | } 63 | else 64 | { 65 | $modelId = $model.modelId 66 | 67 | Write-Host "Found Power BI model '$modelId' for '$dataSetId'" 68 | 69 | Write-Host "Backup '$fileName' into '$backupDir'" 70 | 71 | Copy-Item -Path $filePath -Destination "$backupDir\$fileNameWithoutExt.$(Get-Date -Format "yyyyMMddHHmmss").pbix" -Force 72 | 73 | $zipFile = "$workingDir\$fileName.zip" 74 | 75 | $zipFolder = "$workingDir\$fileNameWithoutExt" 76 | 77 | Write-Host "Unziping '$fileName' into $zipFolder" 78 | 79 | Copy-Item -Path $filePath -Destination $zipFile -Force 80 | 81 | Expand-Archive -Path $zipFile -DestinationPath $zipFolder -Force | Out-Null 82 | 83 | $connectionsJson = Get-Content "$zipFolder\Connections" | ConvertFrom-Json 84 | 85 | $connection = $connectionsJson.Connections[0] 86 | 87 | $remoteArtifactId = $null 88 | 89 | if($connectionsJson.RemoteArtifacts -and $connectionsJson.RemoteArtifacts.Count -ne 0) 90 | { 91 | $remoteArtifactId = $connectionsJson.RemoteArtifacts[0].DatasetId 92 | } 93 | 94 | if ($connection.PbiModelDatabaseName -eq $dataSetId -and $remoteArtifactId -eq $dataSetId) 95 | { 96 | Write-Warning "PBIX '$fileName' already connects to dataset '$dataSetId' skipping the rebind" 97 | return 98 | } 99 | 100 | $connection.PbiServiceModelId = $modelId 101 | $connection.ConnectionString = $connection.ConnectionString.Replace($connection.PbiModelDatabaseName, $dataSetId) 102 | $connection.PbiModelDatabaseName = $dataSetId 103 | 104 | if ($remoteArtifactId) 105 | { 106 | $connectionsJson.RemoteArtifacts[0].DatasetId = $dataSetId 107 | } 108 | 109 | $connectionsJson | ConvertTo-Json -Compress | Out-File "$zipFolder\Connections" -Encoding ASCII 110 | 111 | # Update the connections on zip file 112 | 113 | Write-Host "Updating connections file on zip file" 114 | 115 | Compress-Archive -Path "$zipFolder\Connections" -CompressionLevel Optimal -DestinationPath $zipFile -Update 116 | 117 | # Remove SecurityBindings 118 | 119 | Write-Host "Removing SecurityBindings" 120 | 121 | try{ 122 | $stream = new-object IO.FileStream($zipfile, [IO.FileMode]::Open) 123 | $zipArchive = new-object IO.Compression.ZipArchive($stream, [IO.Compression.ZipArchiveMode]::Update) 124 | $securityBindingsFile = $zipArchive.Entries |? Name -eq "SecurityBindings" | Select -First 1 125 | 126 | if ($securityBindingsFile) 127 | { 128 | $securityBindingsFile.Delete() 129 | } 130 | else 131 | { 132 | Write-Host "Cannot find SecurityBindings on zip" 133 | } 134 | 135 | } 136 | finally{ 137 | if ($zipArchive) { $zipArchive.Dispose() } 138 | if ($stream) { $stream.Dispose() } 139 | } 140 | 141 | Write-Host "Overwriting original pbix" 142 | 143 | Copy-Item -Path $zipfile -Destination $filePath -Force 144 | 145 | } 146 | } 147 | 148 | -------------------------------------------------------------------------------- /Workspace-Clone.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | param( 4 | $sourceWorkspaceId = "e2472ddb-f24f-4b25-ab4b-04537746819c" 5 | , 6 | $targetWorkspaceName = "Test - Clone Workspace (Clone) 2" 7 | , 8 | $reset = $false 9 | ) 10 | 11 | $ErrorActionPreference = "Stop" 12 | 13 | try { Get-PowerBIAccessToken | out-null } catch { Connect-PowerBIServiceAccount } 14 | 15 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 16 | 17 | $outputPath = "$currentPath\Output\WorkspaceClone" 18 | 19 | if ($reset) 20 | { 21 | Get-ChildItem -Path $outputPath -ErrorAction SilentlyContinue | Remove-Item -Force -Recurse 22 | } 23 | 24 | New-Item -ItemType Directory -Path $outputPath -Force -ErrorAction SilentlyContinue | Out-Null 25 | 26 | $sourceWorkspace = Get-PowerBIWorkspace -Id $sourceWorkspaceId 27 | $targetWorkspace = Get-PowerBIWorkspace -Name $targetWorkspaceName 28 | 29 | if (!$targetWorkspace) 30 | { 31 | Write-Host "Creating workspace '$targetWorkspaceName'" 32 | $targetWorkspace = New-PowerBIWorkspace -Name $targetWorkspaceName 33 | } 34 | 35 | #Export Reports 36 | 37 | $sourceReports = Get-PowerBIReport -WorkspaceId $sourceWorkspaceId 38 | $targetReports = Get-PowerBIReport -WorkspaceId $targetWorkspace.Id 39 | 40 | $exportedDatasetIds = @{} 41 | $exportedReportIds = @{} 42 | 43 | foreach ($report in $sourceReports) 44 | { 45 | Write-Host "Processing report '$($report.Name)'" 46 | 47 | $outputFilePath = "$outputPath\$($report.Name).pbix" 48 | 49 | if ($exportedDatasetIds[$report.DatasetId]) { 50 | Write-Host "Dataset $($report.DatasetId) already downloaded" 51 | continue 52 | } 53 | 54 | if (Test-Path $outputFilePath) 55 | { 56 | Write-Host "PBIX was already downloaded." 57 | } 58 | else { 59 | Invoke-PowerBIRestMethod -url "groups/$sourceWorkspaceId/reports/$($report.id)/export" -method Get -OutFile $outputFilePath 60 | } 61 | 62 | if (($targetReports |? { $_.name -eq $report.Name })) 63 | { 64 | Write-Host "PBIX was already imported to target workspace" 65 | } 66 | else 67 | { 68 | Write-Host "Importing PBIX to target workspace" 69 | $importResult = New-PowerBIReport -Path $outputFilePath -WorkspaceId $targetWorkspace.Id -Name $report.Name -ConflictAction CreateOrOverwrite 70 | } 71 | 72 | $targetDataset = Get-PowerBIDataset -WorkspaceId $targetWorkspace.Id |? { $_.name -eq $report.Name } | Select -First 1 73 | $targetReport = Get-PowerBIReport -WorkspaceId $targetWorkspace.Id |? { $_.name -eq $report.Name } | Select -First 1 74 | 75 | $exportedDatasetIds[[string]$report.DatasetId] = [string]$targetDataset.Id 76 | $exportedReportIds[[string]$report.id] = [string]$targetReport.Id 77 | } 78 | 79 | # Cloning reports and rebing to existing dataset 80 | 81 | $otherSourceReports = $sourceReports |? {!($exportedReportIds.Keys -contains $_.id)} 82 | 83 | foreach($report in $otherSourceReports) 84 | { 85 | Write-Host "Cloning report '$($report.Name)'" 86 | 87 | $targetReport = $targetReports |? { $_.name -eq $report.Name } 88 | 89 | if ($targetReport) 90 | { 91 | Write-Host "Report was already cloned" 92 | 93 | $exportedReportIds[[string]$report.id] = [string]$targetReport.Id 94 | } 95 | else { 96 | $body = @{name = $report.Name; targetWorkspaceId = $targetWorkspace.id; targetModelId = $exportedDatasetIds[$report.DatasetId]} | ConvertTo-Json 97 | 98 | $targetReport = Invoke-PowerBIRestMethod -Method Post -Url "groups/$sourceWorkspaceId/reports/$($report.id)/Clone" -Body $body | ConvertFrom-Json 99 | 100 | $exportedReportIds[[string]$report.id] = [string]$targetReport.Id 101 | } 102 | } 103 | 104 | #Cloning dashboards 105 | 106 | $sourceDashboards = Get-PowerBIDashboard -WorkspaceId $sourceWorkspaceId 107 | $targetDashhboards = Get-PowerBIDashboard -WorkspaceId $targetWorkspace.Id 108 | 109 | foreach($dashboard in $sourceDashboards) 110 | { 111 | $targetDashboard = $targetDashhboards |? Name -eq $dashboard.Name 112 | 113 | if (!$targetDashboard) 114 | { 115 | Write-Host "Cloning dashboard '$($dashboard.Name)'" 116 | 117 | $targetDashboard = New-PowerBIDashboard -WorkspaceId $targetWorkspace.Id -Name $dashboard.Name 118 | } 119 | else { 120 | Write-Host "Dashboard already exists" 121 | } 122 | 123 | $dashboardTiles = Get-PowerBIDashboardTile -WorkspaceId $sourceWorkspaceId -DashboardId $dashboard.Id 124 | $targetDashboardTiles = Get-PowerBIDashboardTile -WorkspaceId $targetWorkspace.Id -DashboardId $targetDashboard.Id 125 | 126 | foreach($dashboardTile in $dashboardTiles) 127 | { 128 | if (($targetDashboardTiles |? { $_.Title -eq $dashboardTile.Title })) 129 | { 130 | Write-Host "Tile '$($dashboardTile.Title)' was already cloned" 131 | } 132 | else { 133 | Write-Host "Cloning tile '$($dashboardTile.Title)'" 134 | 135 | $body = @{targetDashboardId = $targetDashboard.Id; targetWorkspaceId = $targetWorkspace.id} 136 | 137 | if ($dashboardTile.ReportId){ 138 | $body["TargetReportId"] = $exportedReportIds[$dashboardTile.ReportId] 139 | } 140 | 141 | if ($dashboardTile.DatasetId) 142 | { 143 | $body["TargetModelId"] = $exportedDatasetIds[$dashboardTile.DatasetId] 144 | } 145 | 146 | $bodyStr = $body | ConvertTo-Json 147 | 148 | $dashboardTileClone = Invoke-PowerBIRestMethod -Method Post -Url "groups/$sourceWorkspaceId/dashboards/$($dashboard.id)/tiles/$($dashboardTile.Id)/Clone" -Body $bodyStr 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /Gateways-AddDatasource.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1026" } 2 | 3 | param( 4 | $gatewayId = "0380ed77-237f-42ec-8f1a-05f2c6cd4a33", 5 | $datasourceName = "Contoso X5", 6 | $datasourceType = "SQL", 7 | $server = "", 8 | $database = "", 9 | $username = "", 10 | $password = "", 11 | # Ensure the Service Principal is added as a Gateway Admin and is allowed to call Power BI Apis 12 | $servicePrincipalId = "", 13 | $servicePrincipalSecret = "", 14 | $servicePrincipalTenantId = "", 15 | # Optional, it will bind this new datasource to existent datasets 16 | $datasetsToBind = @(@{workspaceId="6119d5fa-7cba-4560-b244-37f81946de6b"; datasetId="b41e055d-06fb-4c3d-b775-17b3ff1a4d00"}) 17 | ) 18 | 19 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 20 | 21 | # Install the Power BI package into the current working directory if it's not already installed 22 | if (!(Test-Path "$currentPath\Nuget\Microsoft.PowerBI.Api.3.18.1" -PathType Container)) { 23 | Install-Package -Name "Microsoft.PowerBI.Api" -ProviderName NuGet -Scope CurrentUser -RequiredVersion 3.18.1 -SkipDependencies -Destination "$currentPath\Nuget" -Force 24 | } 25 | 26 | if ($PSVersionTable.PSVersion.Major -le 5) { 27 | $pbipath = Resolve-Path "$currentPath\Nuget\Microsoft.PowerBI.Api.3.18.1\lib\net48\Microsoft.PowerBI.Api.dll" 28 | } 29 | else { 30 | $pbipath = Resolve-Path "$currentPath\Nuget\Microsoft.PowerBI.Api.3.18.1\lib\netstandard2.0\Microsoft.PowerBI.Api.dll" 31 | } 32 | 33 | [System.Reflection.Assembly]::LoadFrom($pbipath) 34 | 35 | if ($servicePrincipalId) 36 | { 37 | $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $servicePrincipalId, ($servicePrincipalSecret | ConvertTo-SecureString -AsPlainText -Force) 38 | 39 | $pbiAccount = Connect-PowerBIServiceAccount -ServicePrincipal -Tenant $servicePrincipalTenantId -Credential $credential 40 | } 41 | else { 42 | $pbiAccount = Connect-PowerBIServiceAccount 43 | } 44 | 45 | Write-Host "Login with: $($pbiAccount.UserName)" 46 | 47 | Write-Host "Getting Gateways" 48 | 49 | $gateways = Invoke-PowerBIRestMethod -url "gateways" -method Get | ConvertFrom-Json | Select -ExpandProperty value 50 | 51 | $gateway = $gateways |? { $_.id -eq $gatewayId } 52 | 53 | if (!$gateway) 54 | { 55 | throw "Cannot find gateway '$gatewayId'" 56 | } 57 | else { 58 | 59 | $datasources = Invoke-PowerBIRestMethod -url "gateways/$gatewayId/datasources" -method Get | ConvertFrom-Json | Select -ExpandProperty value 60 | 61 | $datasource = $datasources |? datasourceName -eq $datasourceName | select -First 1 62 | 63 | if ($datasource) 64 | { 65 | Write-Host "Datasource '$datasourceName' already exists" 66 | } 67 | else 68 | { 69 | Write-Host "Creating Datasource '$datasourceName'" 70 | 71 | # More info about configure credentials: https://learn.microsoft.com/en-us/power-bi/developer/embedded/configure-credentials?tabs=sdk3 72 | 73 | $gatewayKeyObj = [Microsoft.PowerBI.Api.Models.GatewayPublicKey]::new($gateway.publicKey.exponent, $gateway.publicKey.modulus) 74 | $basicCreds = [Microsoft.PowerBI.Api.Models.Credentials.BasicCredentials]::new($username, $password) 75 | $credentialsEncryptor = [Microsoft.PowerBI.Api.Extensions.AsymmetricKeyEncryptor]::new($gatewayKeyObj) 76 | 77 | # Construct the CredentialDetails object. The resulting "Credentials" property on this object will have been encrypted appropriately, ready for use in the request payload. 78 | $credentialDetails = [Microsoft.PowerBI.Api.Models.CredentialDetails]::new( 79 | $basicCreds, 80 | [Microsoft.PowerBI.Api.Models.PrivacyLevel]::Organizational, 81 | [Microsoft.PowerBI.Api.Models.EncryptedConnection]::Encrypted, 82 | $credentialsEncryptor) 83 | 84 | $updateDatasourceBodyStr = "{ 85 | ""dataSourceType"": ""$datasourceType"", 86 | ""connectionDetails"": ""{\""server\"":\""$server\"",\""database\"":\""$database\""}"", 87 | ""datasourceName"": ""$datasourceName"", 88 | ""credentialDetails"": { 89 | ""credentials"": ""$($credentialDetails.Credentials)"", 90 | ""credentialType"": ""Basic"", 91 | ""encryptedConnection"": ""Encrypted"", 92 | ""encryptionAlgorithm"": ""RSA-OAEP"", 93 | ""privacyLevel"": ""Organizational"", 94 | ""useCallerAADIdentity"": ""False"" 95 | } 96 | } 97 | " 98 | 99 | $datasource = Invoke-PowerBIRestMethod -url "gateways/$gatewayId/datasources" -method Post -Body $updateDatasourceBodyStr 100 | 101 | $datasource = $datasource | ConvertFrom-Json 102 | } 103 | } 104 | 105 | if ($datasetsToBind) 106 | { 107 | foreach($dataset in $datasetsToBind) 108 | { 109 | Write-Host "Taking ownership of dataset '$($dataset.datasetId)'" 110 | 111 | Invoke-PowerBIRestMethod -Url "groups/$($dataset.workspaceId)/datasets/$($dataset.datasetId)/Default.TakeOver" -Method Post | Out-Null 112 | 113 | Write-Host "Set dataset '$($dataset.datasetId)' Parameters" 114 | 115 | $bodyStr = @{updateDetails = @(@{name = "Server"; newValue = $server}, @{name = "Database"; newValue = $database})} | ConvertTo-Json 116 | 117 | Invoke-PowerBIRestMethod -Url "groups/$($dataset.workspaceId)/datasets/$($dataset.datasetId)/UpdateParameters" -Body $bodyStr -Method Post | Out-Null 118 | 119 | Write-Host "Bind dataset '$($dataset.datasetId)' to Gateway" 120 | 121 | $bodyStr = @{gatewayObjectId = $datasource.gatewayId; datasourceObjectIds = @($datasource.id)} | ConvertTo-Json 122 | 123 | Invoke-PowerBIRestMethod -Url "groups/$($dataset.workspaceId)/datasets/$($dataset.datasetId)/Default.BindToGateway" -Method Post -Body $bodyStr | Out-Null 124 | } 125 | } -------------------------------------------------------------------------------- /XMLA - Partitions - Create.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | $serverName = "powerbi://api.powerbi.com/v1.0/myorg/Session%20-%20PBI%20on%20Steroids" 3 | , $databaseName = "WWI - Sales (Partitioned)" 4 | , $years = (2013..2016) 5 | ) 6 | 7 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 8 | 9 | Import-Module "$currentPath\TOMHelper.psm1" -Force 10 | 11 | $partitions = @( 12 | @{ 13 | TableName = "Sales" 14 | ; 15 | Partitions = @($years |% { 16 | $year = $_ 17 | 18 | foreach($month in (1..12)) 19 | { 20 | $partitionName = "Sales_$($year)_$($month.ToString().PadLeft(2,"0"))" 21 | 22 | @{ 23 | Name = $partitionName 24 | ; 25 | Type = "M" 26 | ; 27 | Query = "let 28 | Source = Sql.Database(Server, Database, [Query="" 29 | SELECT 30 | [Sale Key] 31 | ,[City Key] 32 | ,[Customer Key] 33 | ,[Bill To Customer Key] 34 | ,[Stock Item Key] 35 | ,[Invoice Date Key] 36 | , case when (ABS(CAST(CAST(NEWID() AS VARBINARY) AS INT)) % 10) > 5 then 37 | DATEADD(DAY, ABS(CAST(CAST(NEWID() AS VARBINARY) AS INT)) % 300, [Invoice Date Key]) else 38 | null end 39 | [Invoice Paid Date Key] 40 | ,[Delivery Date Key] 41 | , e.[WWI Employee ID] [Employee Key] 42 | ,[Quantity] 43 | ,[Unit Price] 44 | ,[Total Excluding Tax] [Total Amount] 45 | ,[Tax Amount] 46 | ,[Profit] 47 | FROM [Fact].[Sale] s 48 | left join [Dimension].[Employee] e on e.[Employee Key] = s.[Salesperson Key] 49 | WHERE year([Invoice Date Key]) = $year and month([Invoice Date Key]) = $month""]) 50 | in 51 | Source" 52 | } 53 | } 54 | 55 | }) 56 | } 57 | , 58 | @{ 59 | TableName = "Orders" 60 | ; 61 | Partitions = @($years |% { 62 | $year = $_ 63 | 64 | $partitionName = "Orders_$($year)" 65 | 66 | @{ 67 | Name = $partitionName 68 | ; 69 | Type = "M" 70 | ; 71 | Query = "let 72 | Source = Sql.Database (Server, Database), 73 | Table = Source{[Schema=""Fact"",Item=""Order""]}[Data], 74 | #""Filtered Rows"" = Table.SelectRows(Table, each Date.Year([Order Date Key]) = $year), 75 | KeepColumns = Table.SelectColumns(#""Filtered Rows"",{""Order Date Key"", ""City Key"", ""Customer Key"", ""Stock Item Key"", ""Salesperson Key"", ""Quantity"", ""Unit Price"", ""Tax Rate"", ""Total Excluding Tax"", ""Tax Amount"", ""Total Including Tax""}) 76 | in 77 | KeepColumns" 78 | } 79 | 80 | }) 81 | } 82 | , 83 | @{ 84 | TableName = "Transactions" 85 | ; 86 | Partitions = @($years |% { 87 | $year = $_ 88 | 89 | $partitionName = "Transactions_$($year)" 90 | 91 | @{ 92 | Name = $partitionName 93 | ; 94 | Type = "M" 95 | ; 96 | Query = "let 97 | Source = Sql.Database(Server, Database), 98 | Table = Source{[Schema=""Fact"",Item=""Transaction""]}[Data], 99 | #""Expanded Dimension.Payment Method"" = Table.ExpandRecordColumn(Table, ""Dimension.Payment Method"", {""Payment Method""}, {""Dimension.Payment Method.Payment Method""}), 100 | #""Filtered Rows"" = Table.SelectRows(#""Expanded Dimension.Payment Method"", each Date.Year([Date Key]) = $year), 101 | KeepColumns = Table.SelectColumns(#""Filtered Rows"",{""Date Key"", ""Dimension.Payment Method.Payment Method"", ""Customer Key"", ""Bill To Customer Key"", ""Supplier Key"", ""Transaction Type Key"", ""Payment Method Key"", ""Total Excluding Tax"", ""Total Including Tax"", ""Is Finalized""}), 102 | #""Changed Type"" = Table.TransformColumnTypes(KeepColumns,{{""Total Including Tax"", Currency.Type}, {""Total Excluding Tax"", Currency.Type}}), 103 | #""Renamed Columns"" = Table.RenameColumns(#""Changed Type"",{{""Dimension.Payment Method.Payment Method"", ""Payment Method""}}) 104 | in 105 | #""Renamed Columns""" 106 | } 107 | 108 | }) 109 | } 110 | , 111 | @{ 112 | TableName = "Purchases" 113 | ; 114 | Partitions = @($years |% { 115 | 116 | $year = $_ 117 | 118 | $partitionName = "Purchases_$($year)" 119 | 120 | @{ 121 | Name = $partitionName 122 | ; 123 | Type = "M" 124 | ; 125 | Query = "let 126 | Source = Sql.Database(Server, Database), 127 | Fact_Purchase = Source{[Schema=""Fact"",Item=""Purchase""]}[Data], 128 | #""Filtered Rows"" = Table.SelectRows(Fact_Purchase, each Date.Year([Date Key]) = $year), 129 | #""Removed Other Columns"" = Table.SelectColumns(#""Filtered Rows"",{""Purchase Key"", ""Date Key"", ""Supplier Key"", ""Stock Item Key"", ""Ordered Quantity"", ""Is Order Finalized""}), 130 | #""Renamed Columns"" = Table.RenameColumns(#""Removed Other Columns"",{{""Date Key"", ""Purchase Date Key""}}) 131 | in 132 | #""Renamed Columns""" 133 | } 134 | 135 | }) 136 | } 137 | 138 | ) 139 | 140 | $results = Add-ASTablePartition -serverName $serverName -databaseName $databaseName -partitions $partitions -removeDefaultPartition -Verbose 141 | 142 | $results -------------------------------------------------------------------------------- /Admin-TenantScan.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Modules @{ ModuleName="MicrosoftPowerBIMgmt"; ModuleVersion="1.2.1077" } 2 | 3 | param ( 4 | $modifiedSince = $null, #[datetime]::UtcNow.Date.AddDays(-10), 5 | $getInfoDetails = "getArtifactUsers=true&lineage=true&datasourceDetails=true&datasetSchema=true&datasetExpressions=true", 6 | $excludePersonalWorkspaces = $false, 7 | $excludeInActiveWorkspaces = $true, 8 | $outputPath = ".\output\tenantscan", 9 | $servicePrincipalId = "", 10 | $servicePrincipalSecret = "", 11 | $servicePrincipalTenantId = "" 12 | ) 13 | 14 | #region Functions 15 | 16 | function Get-ArrayInBatches 17 | { 18 | [cmdletbinding()] 19 | param 20 | ( 21 | [array]$array 22 | , 23 | [int]$batchCount 24 | , 25 | [ScriptBlock]$script 26 | , 27 | [string]$label = "Get-ArrayInBatches" 28 | ) 29 | 30 | $skip = 0 31 | 32 | do 33 | { 34 | $batchItems = @($array | Select -First $batchCount -Skip $skip) 35 | 36 | if ($batchItems) 37 | { 38 | Write-Host "[$label] Batch: $($skip + $batchCount) / $($array.Count)" 39 | 40 | Invoke-Command -ScriptBlock $script -ArgumentList @(,$batchItems) 41 | 42 | $skip += $batchCount 43 | } 44 | 45 | } 46 | while($batchItems.Count -ne 0 -and $batchItems.Count -ge $batchCount) 47 | } 48 | 49 | function Wait-On429Error 50 | { 51 | [cmdletbinding()] 52 | param 53 | ( 54 | [ScriptBlock]$script 55 | , 56 | [int]$sleepSeconds = 3601 57 | , 58 | [int]$tentatives = 1 59 | ) 60 | 61 | try { 62 | 63 | Invoke-Command -ScriptBlock $script 64 | 65 | } 66 | catch { 67 | 68 | $ex = $_.Exception 69 | 70 | if ($ex.ToString().Contains("429 (Too Many Requests)")) { 71 | Write-Host "'429 (Too Many Requests)' Error - Sleeping for $sleepSeconds seconds before trying again" -ForegroundColor Yellow 72 | 73 | $tentatives = $tentatives - 1 74 | 75 | if ($tentatives -lt 0) 76 | { 77 | throw "[Wait-On429Error] Max Tentatives reached!" 78 | } 79 | else 80 | { 81 | Start-Sleep -Seconds $sleepSeconds 82 | 83 | Wait-On429Error -script $script -sleepSeconds $sleepSeconds -tentatives $tentatives 84 | } 85 | } 86 | else { 87 | throw 88 | } 89 | } 90 | } 91 | 92 | #endregion 93 | 94 | $ErrorActionPreference = "Stop" 95 | 96 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 97 | 98 | Set-Location $currentPath 99 | 100 | $scansOutputPath = Join-Path $outputPath ("{0:yyyy}\{0:MM}\{0:dd}" -f [datetime]::Today) 101 | 102 | New-Item -ItemType Directory -Path $scansOutputPath -ErrorAction SilentlyContinue | Out-Null 103 | 104 | try { 105 | $token = Get-PowerBIAccessToken 106 | } 107 | catch { 108 | if ($servicePrincipalId) 109 | { 110 | $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $servicePrincipalId, ($servicePrincipalSecret | ConvertTo-SecureString -AsPlainText -Force) 111 | 112 | $pbiAccount = Connect-PowerBIServiceAccount -ServicePrincipal -Tenant $servicePrincipalTenantId -Credential $credential 113 | } 114 | else { 115 | $pbiAccount = Connect-PowerBIServiceAccount 116 | } 117 | } 118 | 119 | $modifiedRequestUrl = "admin/workspaces/modified?excludePersonalWorkspaces=$excludePersonalWorkspaces&excludeInActiveWorkspaces=$excludeInActiveWorkspaces" 120 | 121 | if ($modifiedSince) 122 | { 123 | $modifiedRequestUrl = $modifiedRequestUrl + "&modifiedSince=$($modifiedSince.ToString("o"))" 124 | $fullScan = $false 125 | } 126 | else 127 | { 128 | $fullScan = $true 129 | } 130 | 131 | Write-Host "WorkspacesModified: $modifiedRequestUrl" 132 | 133 | # Delete existent fullscans 134 | 135 | if ($fullScan) 136 | { 137 | Write-Host "Removing full scans" 138 | Get-ChildItem -Path "$scansOutputPath\*fullscan*" | Remove-Item -Force 139 | } 140 | 141 | $workspacesModified = Invoke-PowerBIRestMethod -Url $modifiedRequestUrl -Method Get | ConvertFrom-Json 142 | 143 | if (!$workspacesModified -or $workspacesModified.Count -eq 0) 144 | { 145 | Write-Host "No workspaces modified" 146 | } 147 | else { 148 | Write-Host "Modified workspaces: $($workspacesModified.Count)" 149 | 150 | $throttleErrorSleepSeconds = 3700 151 | $scanStatusSleepSeconds = 5 152 | $getInfoOuterBatchCount = 1500 153 | $getInfoInnerBatchCount = 100 154 | 155 | Write-Host "Throttle Handling Variables: getInfoOuterBatchCount: $getInfoOuterBatchCount; getInfoInnerBatchCount: $getInfoInnerBatchCount; throttleErrorSleepSeconds: $throttleErrorSleepSeconds" 156 | 157 | Get-ArrayInBatches -array $workspacesModified -label "GetInfo Global Batch" -batchCount $getInfoOuterBatchCount -script { 158 | param($workspacesModifiedOuterBatch) 159 | 160 | $script:workspacesScanRequests = @() 161 | 162 | # Call GetInfo in batches of 100 (MAX 500 requests per hour) 163 | 164 | Get-ArrayInBatches -array $workspacesModifiedOuterBatch -label "GetInfo Local Batch" -batchCount $getInfoInnerBatchCount -script { 165 | param($workspacesBatch) 166 | 167 | Wait-On429Error -tentatives 1 -sleepSeconds $throttleErrorSleepSeconds -script { 168 | 169 | $bodyStr = @{"workspaces" = @($workspacesBatch.Id) } | ConvertTo-Json 170 | 171 | # $script: scope to reference the outerscope variable 172 | 173 | $getInfoResult = @(Invoke-PowerBIRestMethod -Url "admin/workspaces/getInfo?$getInfoDetails" -Body $bodyStr -method Post | ConvertFrom-Json) 174 | 175 | $script:workspacesScanRequests += $getInfoResult 176 | 177 | } 178 | } 179 | 180 | # Wait for Scan to execute - https://docs.microsoft.com/en-us/rest/api/power-bi/admin/workspaceinfo_getscanstatus (10,000 requests per hour) 181 | 182 | while(@($workspacesScanRequests |? status -in @("Running", "NotStarted"))) 183 | { 184 | Write-Host "Waiting for scan results, sleeping for $scanStatusSleepSeconds seconds..." 185 | 186 | Start-Sleep -Seconds $scanStatusSleepSeconds 187 | 188 | foreach ($workspaceScanRequest in $workspacesScanRequests) 189 | { 190 | $scanStatus = Invoke-PowerBIRestMethod -Url "admin/workspaces/scanStatus/$($workspaceScanRequest.id)" -method Get | ConvertFrom-Json 191 | 192 | Write-Host "Scan '$($scanStatus.id)' : '$($scanStatus.status)'" 193 | 194 | $workspaceScanRequest.status = $scanStatus.status 195 | } 196 | } 197 | 198 | # Get Scan results (500 requests per hour) - https://docs.microsoft.com/en-us/rest/api/power-bi/admin/workspaceinfo_getscanresult 199 | 200 | foreach ($workspaceScanRequest in $workspacesScanRequests) 201 | { 202 | Wait-On429Error -tentatives 1 -sleepSeconds $throttleErrorSleepSeconds -script { 203 | 204 | $scanResult = Invoke-PowerBIRestMethod -Url "admin/workspaces/scanResult/$($workspaceScanRequest.id)" -method Get | ConvertFrom-Json 205 | 206 | Write-Host "Scan Result'$($scanStatus.id)' : '$($scanResult.workspaces.Count)'" 207 | 208 | $fullScanSuffix = ".scan" 209 | 210 | if ($fullScan) 211 | { 212 | $fullScanSuffix = ".fullscan" 213 | } 214 | 215 | $outputFilePath = "$scansOutputPath\$($workspaceScanRequest.id)$fullScanSuffix.json" 216 | 217 | $scanResult | Add-Member –MemberType NoteProperty –Name "scanCreatedDateTime" –Value $workspaceScanRequest.createdDateTime -Force 218 | 219 | ConvertTo-Json $scanResult -Depth 10 -Compress | Out-File $outputFilePath -force 220 | } 221 | 222 | } 223 | } 224 | 225 | } -------------------------------------------------------------------------------- /TOMHelper.psm1: -------------------------------------------------------------------------------- 1 | $script:azureDevOpsLogs = $false 2 | 3 | $currentPath = (Split-Path $MyInvocation.MyCommand.Definition -Parent) 4 | 5 | Write-Host "Loading Module Assemblies" 6 | 7 | # Downloading Nugets 8 | 9 | if ($PSVersionTable.PSVersion.Major -le 5) { 10 | $nugetName = "Microsoft.AnalysisServices.retail.amd64" 11 | $nugetVersion = "19.48.0" 12 | 13 | if (!(Test-Path "$currentPath\Nuget\$nugetName.$nugetVersion" -PathType Container)) { 14 | Install-Package -Name $nugetName -ProviderName NuGet -Scope CurrentUser -RequiredVersion $nugetVersion -SkipDependencies -Destination "$currentPath\Nuget" -Force 15 | } 16 | 17 | $dllPath = "$currentPath\Nuget\$nugetName.$nugetVersion\lib\net45\Microsoft.AnalysisServices.Tabular.dll" 18 | 19 | Add-Type -Path $dllPath 20 | } 21 | else 22 | { 23 | # need to load the Microsoft.Identity.Client, otherwise the Connect fails with 'Connection cannot be made' 24 | 25 | $nugetName = "Microsoft.Identity.Client" 26 | $nugetVersion = "4.43.0" 27 | 28 | if (!(Test-Path "$currentPath\Nuget\$nugetName.$nugetVersion" -PathType Container)) { 29 | Install-Package -Name $nugetName -ProviderName NuGet -Scope CurrentUser -RequiredVersion $nugetVersion -SkipDependencies -Destination "$currentPath\Nuget" -Force 30 | } 31 | 32 | $dllPath = "$currentPath\Nuget\$nugetName.$nugetVersion\lib\netcoreapp2.1\Microsoft.Identity.Client.dll" 33 | 34 | Add-Type -Path $dllPath 35 | 36 | $nugetName = "Microsoft.AnalysisServices.NetCore.retail.amd64" 37 | $nugetVersion = "19.48.0" 38 | 39 | if (!(Test-Path "$currentPath\Nuget\$nugetName.$nugetVersion" -PathType Container)) { 40 | Install-Package -Name $nugetName -ProviderName NuGet -Scope CurrentUser -RequiredVersion $nugetVersion -SkipDependencies -Destination "$currentPath\Nuget" -Force 41 | } 42 | 43 | $dllPath = "$currentPath\Nuget\$nugetName.$nugetVersion\lib\netcoreapp3.0\Microsoft.AnalysisServices.Tabular.dll" 44 | 45 | Add-Type -Path $dllPath 46 | } 47 | 48 | $asModelType = [Microsoft.AnalysisServices.Tabular.Model] 49 | 50 | $assembly = $asModelType.Assembly 51 | 52 | Write-Host "Assembly version loaded: '$($assembly.FullName)' from '$($assembly.Location)'" 53 | 54 | Function Set-ModuleConfig 55 | { 56 | [CmdletBinding()] 57 | param 58 | ( 59 | [bool] $azureDevOpsLogs 60 | ) 61 | 62 | if ($azureDevOpsLogs -ne $script:azureDevOpsLogs) 63 | { 64 | Write-Host "Changing 'azureDevOpsLogs' from '$($script:azureDevOpsLogs)' to '$azureDevOpsLogs'" 65 | 66 | $script:azureDevOpsLogs = $azureDevOpsLogs 67 | } 68 | } 69 | 70 | Function Invoke-XMLAScript 71 | { 72 | param 73 | ( 74 | [Parameter(Mandatory = $true)] 75 | [string]$serverName 76 | , 77 | [Parameter(ParameterSetName = 'script', Mandatory = $true)] 78 | [string]$xmlaScript 79 | , 80 | [Parameter(ParameterSetName = 'file', Mandatory = $true)] 81 | [string]$xmlaScriptFilePath 82 | , 83 | [System.Text.Encoding] $encoding = [System.Text.Encoding]::Default 84 | , 85 | [string] $username 86 | , 87 | [string] $password 88 | , 89 | [string] $authToken 90 | ) 91 | 92 | try 93 | { 94 | $server = Connect-ASServer -serverName $serverName -userId $username -password $password -authToken $authToken 95 | 96 | Write-Log "Executing XMLA on on '$serverName'" 97 | 98 | if (![string]::IsNullOrEmpty($xmlaScriptFilePath)) 99 | { 100 | $xmlaScript = [IO.File]::ReadAllText($xmlaScriptFilePath, $encoding) 101 | } 102 | 103 | $result = $server.Execute($xmlaScript) 104 | 105 | if ($result.ContainsErrors -eq $true) 106 | { 107 | $strErrors = "" 108 | 109 | # Get Messages 110 | 111 | $nl = [System.Environment]::NewLine 112 | 113 | foreach($xr in $result.Messages) 114 | { 115 | $strErrors += $xr.Description 116 | $strErrors += $nl 117 | } 118 | 119 | throw "Error executing Deploy to Server: $($nl)$($strErrors)" 120 | } 121 | 122 | } 123 | finally 124 | { 125 | if ($server) 126 | { 127 | $server.Dispose() 128 | } 129 | } 130 | } 131 | 132 | Function Invoke-ASTableProcess 133 | { 134 | <# 135 | .SYNOPSIS 136 | Issues a Process Command for the specified database/table/partition 137 | .DESCRIPTION 138 | 139 | .PARAMETER connStr 140 | Connection String to the tabular database 141 | .PARAMETER tables 142 | Collection of tables to add partitions 143 | Ex: @{ 144 | DatabaseName = "DBName" 145 | ; 146 | TableName = "Table" 147 | ; 148 | Partitions = @() 149 | } 150 | .PARAMETER resetPartitions 151 | Resets all the partitions for every table specified 152 | .EXAMPLE 153 | Invoke-ASTableProcess -tables @( 154 | @{ 155 | Server = $connStr 156 | ; 157 | DatabaseName = $databaseName 158 | ; 159 | TableName = "Sales" 160 | ; 161 | Partitions = @(($refDate.AddMonths(-2).Month..$refDate.Month) |% { $refDate.Year.ToString() + $_.ToString().PadLeft(2,'0') }) 162 | ; 163 | Enabled = 1 164 | ; 165 | Group = "G2" 166 | }) 167 | #> 168 | [CmdletBinding()] 169 | param 170 | ( 171 | [array]$tables = @( 172 | @{ 173 | Server = "" 174 | ; 175 | DatabaseName = "" 176 | ; 177 | TableName = "" 178 | ; 179 | Partitions = @("ALL") 180 | ; 181 | Enabled = 1 182 | ; 183 | Group = "G1" 184 | } 185 | ) 186 | , 187 | [int] $maxParallelism = 5 188 | , 189 | [string] $executionCode 190 | , 191 | [string] $executionsPath 192 | , 193 | [string] $username 194 | , 195 | [string] $password 196 | ) 197 | 198 | try 199 | { 200 | Write-Log "Starting AS Process" 201 | 202 | $executionResult = @() 203 | 204 | if ($executionsPath -and $executionCode) 205 | { 206 | New-Item -Path $executionsPath -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null 207 | 208 | $executionFilePath = "$executionsPath\$executionCode.json" 209 | 210 | if (Test-Path $executionFilePath) 211 | { 212 | Write-Log "Found active execution '$executionCode'" 213 | 214 | $previousExecutionResult = Get-Content -Path $executionFilePath | ConvertFrom-Json 215 | 216 | $previousExecutionProcessOrders = $previousExecutionResult | Select ProcessGroup,ProcessStatus -ExpandProperty ProcessOrders 217 | } 218 | 219 | } 220 | 221 | # Set default values 222 | 223 | $tables |% { 224 | if (!$_.RefreshType) 225 | { 226 | $_.RefreshType = "full" 227 | } 228 | } 229 | 230 | # Group by server & database 231 | 232 | $tables | Group-Object {$_.Server } |% { 233 | 234 | $serverName = $_.Name 235 | 236 | $connStr = $serverName 237 | 238 | $server = Connect-ASServer -serverName $connStr -userId $username -password $password 239 | 240 | $_.Group | Group-Object { $_.DatabaseName + "|" + $_.Group } | Sort-Object { $_.Name } |% { 241 | 242 | $databaseName = $_.Name.Split('|')[0] 243 | $group = $_.Name.Split('|')[1] 244 | 245 | Write-Log "StartTime: $((Get-Date -Format "yyyy-MM-dd HH:mm:ss.ms").Substring(0, 22))" 246 | Write-Log "Processing database '$databaseName' on group '$group'" 247 | 248 | $startTime = [datetime]::UtcNow 249 | 250 | try 251 | { 252 | 253 | $processError = $null 254 | 255 | $database = $server.Databases.GetByName($databaseName) 256 | 257 | # Iterate the tables 258 | 259 | $processOrders = $_.Group 260 | 261 | # Create the process execution to save results 262 | 263 | $processExecution = @{ 264 | ProcessGroup = $group 265 | ; 266 | ProcessOrders = @() 267 | ; 268 | ProcessStartDate = $startTime.ToString("s") 269 | } 270 | 271 | $executionResult += $processExecution 272 | 273 | $processOrders | Sort-Object { 274 | 275 | $processType = $_.RefreshType 276 | 277 | # Define the order of the process 278 | 279 | $refreshTypes = @("full", "dataonly", "clearvalues", "automatic", "calculate","defragment") 280 | 281 | $refreshTypes.IndexOf($processType.ToLower()) 282 | 283 | } |% { 284 | 285 | $tableName = $_.TableName 286 | 287 | $processType = $_.RefreshType 288 | 289 | if ($tableName -eq "ALL" -or !$tableName) 290 | { 291 | Write-Log "Process '$processType' on database '$databaseName'" 292 | 293 | $alreadyProcessed = @($previousExecutionProcessOrders |? { ($_.ProcessStatus -in @("Processed") -or !$_.Skipped) -and $_.RefreshType -ne "calculate" -and $_.RefreshType -eq $processType -and $_.Server -eq $serverName -and $_.Database -eq $databaseName -and !$_.Table -and !$_.Partition}).Count -gt 0 294 | 295 | $processOrder = @{Server = $serverName; Database = $databaseName; RefreshType = $processType} 296 | 297 | $processExecution.ProcessOrders += $processOrder 298 | 299 | if (!$alreadyProcessed) 300 | { 301 | $database.Model.RequestRefresh($processType, $null) 302 | } 303 | else 304 | { 305 | $processOrder.Skipped = $true 306 | Write-Log "Skipped, already processed in previous execution" 307 | } 308 | } 309 | else 310 | { 311 | $table = $database.Model.Tables.Find($tableName) 312 | 313 | if ($table -eq $null) 314 | { 315 | throw "Cannot find table '$tableName'" 316 | } 317 | 318 | $partitions = $_.Partitions 319 | 320 | if($partitions -eq $null -or $partitions -contains "ALL") 321 | { 322 | Write-Log "Process '$processType' on table '$tableName'" 323 | 324 | $alreadyProcessed = @($previousExecutionProcessOrders |? { ($_.ProcessStatus -in @("Processed") -or !$_.Skipped) -and $_.RefreshType -eq $processType -and $_.Server -eq $serverName -and $_.Database -eq $databaseName -and $_.Table -eq $tableName -and !$_.Partition}).Count -gt 0 325 | 326 | $processOrder = @{Server = $serverName; Database = $databaseName; Table=$tableName; RefreshType = $processType} 327 | $processExecution.ProcessOrders += $processOrder 328 | 329 | if (!$alreadyProcessed) 330 | { 331 | $table.RequestRefresh($processType, $null) 332 | } 333 | else 334 | { 335 | $processOrder.Skipped = $true 336 | Write-Log "Skipped, already processed in previous execution" 337 | } 338 | } 339 | else 340 | { 341 | $partitionsToProcess = @($table.Partitions |? { 342 | 343 | $partitionName = $_.Name 344 | 345 | if ($partitions -contains $partitionName) 346 | { 347 | return $true 348 | } 349 | 350 | return $false 351 | }) 352 | 353 | 354 | $partitionsToProcess |% { 355 | 356 | $partitionName = $_.Name 357 | 358 | Write-Log "Process '$processType' on partition: '$partitionName', table: '$tableName', database: '$databaseName'" 359 | 360 | $alreadyProcessed = @($previousExecutionProcessOrders |? { ($_.ProcessStatus -in @("Processed") -or !$_.Skipped) -and $_.RefreshType -eq $processType -and $_.Server -eq $serverName -and $_.Database -eq $databaseName -and $_.Table -eq $tableName -and $_.Partition -eq $partitionName}).Count -gt 0 361 | 362 | $processOrder = @{Server = $serverName; Database = $databaseName; Table=$tableName; Partition = $partitionName; RefreshType = $processType} 363 | 364 | $processExecution.ProcessOrders += $processOrder 365 | 366 | if (!$alreadyProcessed) 367 | { 368 | $_.RequestRefresh($processType, $null) 369 | } 370 | else 371 | { 372 | $processOrder.Skipped = $true 373 | Write-Log "Skipped, already processed in previous execution" 374 | } 375 | } 376 | } 377 | 378 | } 379 | } 380 | 381 | 382 | $groupMaxParallelism = (@($processOrders |? {$_.MaxParallelism} |% { $_.MaxParallelism }) | Measure -Maximum).Maximum 383 | 384 | if (!$groupMaxParallelism) 385 | { 386 | $groupMaxParallelism = $maxParallelism 387 | } 388 | 389 | $processExecution.MaxParallelism = $groupMaxParallelism 390 | 391 | $xmlaResult = Save-ASDatabaseChanges $database -maxParallelism $groupMaxParallelism 392 | 393 | } 394 | catch 395 | { 396 | $processError = $_.Exception.ToString() 397 | 398 | Write-Log "Error processing $processError" -Level Error 399 | 400 | Undo-ASDatabaseChanges $database | Out-Null 401 | } 402 | finally 403 | { 404 | $endTime = [datetime]::UtcNow 405 | 406 | Write-Log "Time Elapsed: $(($endTime-$startTime).TotalSeconds)s; EndTime: $((Get-Date -Format "yyyy-MM-dd HH:mm:ss.ms").Substring(0, 22))" 407 | 408 | if ($processError -eq $null) 409 | { 410 | if ($processExecution.ProcessOrders.Count -eq 0) 411 | { 412 | $processStatus = "NoProcessOrders" 413 | } 414 | else 415 | { 416 | $processStatus = "Processed" 417 | } 418 | } 419 | else { 420 | $processStatus = "Error" 421 | } 422 | 423 | $processExecution.ProcessEndDate = $endTime.ToString("s") 424 | $processExecution.ProcessDuration = ($endTime - $startTime).TotalSeconds 425 | $processExecution.ProcessError = $processError 426 | $processExecution.ProcessStatus = $processStatus 427 | 428 | if ($executionFilePath) 429 | { 430 | Write-Log "Saving execution" 431 | $executionResult | ConvertTo-Json -Depth 5 | Out-File $executionFilePath 432 | } 433 | 434 | Write-Output $processExecution 435 | } 436 | } 437 | } 438 | } 439 | finally 440 | { 441 | if ($server) 442 | { 443 | $server.Dispose() 444 | } 445 | } 446 | } 447 | 448 | Function Get-ASTable 449 | { 450 | [CmdletBinding()] 451 | param 452 | ( 453 | [Parameter(Mandatory=$true)] 454 | [string] $serverName 455 | , 456 | [Parameter(Mandatory=$true)] 457 | [string] $databaseName 458 | , 459 | [string] $tableName 460 | , 461 | [string] $username 462 | , 463 | [string] $password 464 | , 465 | [Switch]$includePartitions 466 | ) 467 | 468 | try { 469 | 470 | $server = Connect-ASServer -serverName $serverName -userId $username -password $password 471 | 472 | $database = $server.Databases.GetByName($databaseName) 473 | 474 | $tables = @() 475 | 476 | if (![string]::IsNullOrEmpty($tableName)) 477 | { 478 | $table = $database.Model.Tables.Find($tableName) 479 | 480 | if (!$table) 481 | { 482 | throw "Cannot find table '$tableName'" 483 | } 484 | 485 | $tables += $table 486 | } 487 | else { 488 | $tables += $database.Model.Tables 489 | } 490 | 491 | foreach($table in $tables) 492 | { 493 | $outputObj = @{ 494 | Name = $table.Name 495 | } 496 | 497 | if($includePartitions) 498 | { 499 | $outputObj.Partitions = @() 500 | 501 | foreach($partition in $table.Partitions) 502 | { 503 | $outputObjPartition = @{ 504 | "Name" = $partition.Name 505 | ; 506 | "RefreshedTime" = $partition.RefreshedTime 507 | ; 508 | "State" = $partition.State 509 | ; 510 | "Type" = $partition.SourceType.ToString() 511 | } 512 | 513 | if ($partition.SourceType -eq [Microsoft.AnalysisServices.Tabular.PartitionSourceType]::Query) 514 | { 515 | $outputObjPartition.Query = $partition.Source.Query 516 | } 517 | else 518 | { 519 | $outputObjPartition.Query = $partition.Source.Expression 520 | } 521 | 522 | $outputObj.Partitions += $outputObjPartition 523 | } 524 | } 525 | 526 | Write-Output $outputObj 527 | } 528 | } 529 | finally { 530 | 531 | if ($database) 532 | { 533 | $database.Dispose() 534 | } 535 | 536 | if ($server) 537 | { 538 | $server.Dispose() 539 | } 540 | } 541 | } 542 | 543 | Function Add-ASTablePartition 544 | { 545 | [CmdletBinding()] 546 | param 547 | ( 548 | [string] $serverName 549 | , 550 | [string] $databaseName 551 | , 552 | [string] $bimFilePath 553 | , 554 | [array] $partitions = @( 555 | @{ 556 | TableName = "Table" 557 | ; 558 | Partitions = @() 559 | }) 560 | , 561 | [switch] $resetPartitions 562 | , 563 | [switch] $removeDefaultPartition 564 | , 565 | [string] $username 566 | , 567 | [string] $password 568 | ) 569 | 570 | try 571 | { 572 | 573 | Write-Log "Starting AS Partition Creation" 574 | 575 | $server = $null 576 | 577 | if ([string]::IsNullOrEmpty($bimFilePath)) 578 | { 579 | $server = Connect-ASServer -serverName $serverName -userId $username -password $password 580 | 581 | $database = $server.Databases.GetByName($databaseName) 582 | } 583 | elseif (![string]::IsNullOrEmpty($bimFilePath)) 584 | { 585 | $databaseStr = [IO.File]::ReadAllText($bimFilePath) 586 | 587 | $database = [Microsoft.AnalysisServices.Tabular.JsonSerializer]::DeserializeDatabase($databaseStr) 588 | } 589 | else 590 | { 591 | throw "Must specify -server & -database or -bimFilePath" 592 | } 593 | 594 | $partitions | Group-Object { $_.TableName } |% { 595 | 596 | $tableName = $_.Name 597 | $tablePartitions = $_.Group 598 | 599 | $table = $database.Model.Tables.Find($tableName) 600 | 601 | if ($table -eq $null) 602 | { 603 | throw "Cannot find table '$tableName'" 604 | } 605 | 606 | if ($resetPartitions) 607 | { 608 | $table.Partitions.Clear(); 609 | } 610 | 611 | # If only has the default partition remove it 612 | 613 | if ($removeDefaultPartition -and $table.Partitions.Count -eq 1 -and $table.Partitions[0].Name.StartsWith("$($table.Name)-")) 614 | { 615 | $table.Partitions.Clear(); 616 | } 617 | 618 | $tablePartitions |% { 619 | 620 | $part = $_ 621 | 622 | $partition = $table.Partitions.Find($part.Name) 623 | 624 | if ($partition -ne $null) 625 | { 626 | Write-Log "Updating Partition '$($part.Name)' on table '$tableName'" 627 | } 628 | else 629 | { 630 | Write-Log "Creating Partition '$($part.Name)' on table '$tableName'" 631 | 632 | $partition = new-object Microsoft.AnalysisServices.Tabular.Partition 633 | 634 | $partition.Name = $part.Name 635 | 636 | $table.Partitions.Add($partition) 637 | } 638 | 639 | if ($part.Type -eq "M") 640 | { 641 | $source = new-object Microsoft.AnalysisServices.Tabular.MPartitionSource 642 | 643 | $source.Expression = $part.Query 644 | } 645 | elseif($part.Type -eq "Legacy") 646 | { 647 | $source = new-object Microsoft.AnalysisServices.Tabular.QueryPartitionSource 648 | 649 | $dataSource = $database.Model.DataSources.Find($part.DataSourceName) 650 | 651 | if ($dataSource -eq $null) 652 | { 653 | throw "Cannot find datasource '$($part.DataSourceName)', legacy partitions must specify a valid datasource name." 654 | } 655 | 656 | $source.DataSource = $dataSource 657 | 658 | $source.Query = $part.Query 659 | } 660 | else { 661 | throw "Invalid partition type: '$($part.Type)'" 662 | } 663 | 664 | $partition.Source = $source 665 | 666 | } 667 | 668 | if ($server) 669 | { 670 | Save-ASDatabaseChanges $database 671 | } 672 | else 673 | { 674 | $databaseStr = [Microsoft.AnalysisServices.Tabular.JsonSerializer]::SerializeDatabase($database) 675 | 676 | $databaseStr | Out-File $bimFilePath -Force 677 | } 678 | 679 | } 680 | } 681 | finally 682 | { 683 | if ($database) 684 | { 685 | $database.Dispose() 686 | } 687 | 688 | if ($server) 689 | { 690 | $server.Dispose() 691 | } 692 | } 693 | } 694 | 695 | Function Remove-ASTablePartition 696 | { 697 | [CmdletBinding()] 698 | param 699 | ( 700 | [string]$serverName 701 | , 702 | [string]$databaseName 703 | , 704 | [string]$tableName 705 | , 706 | [array]$partitions = @() 707 | , 708 | [string] $username 709 | , 710 | [string] $password 711 | ) 712 | 713 | try 714 | { 715 | $server = Connect-ASServer -serverName $serverName -userId $username -password $password 716 | 717 | $database = $server.Databases.GetByName($databaseName) 718 | 719 | $table = $database.Model.Tables.Find($tableName) 720 | 721 | if ($table -eq $null) 722 | { 723 | throw "Cannot find table '$tableName'" 724 | } 725 | 726 | $partitions |% { 727 | 728 | $partitionName = $_ 729 | 730 | $partition = $table.Partitions.Find($partitionName) 731 | 732 | if ($partition -eq $null) 733 | { 734 | Write-Log "Cannot find Partition '$partitionName'" 735 | } 736 | 737 | Write-Log "Removing partition '$partitionName'" 738 | 739 | $table.Partitions.Remove($partitionName) 740 | } 741 | 742 | Save-ASDatabaseChanges $database 743 | 744 | } 745 | finally 746 | { 747 | if ($server) 748 | { 749 | $server.Dispose() 750 | } 751 | } 752 | } 753 | 754 | Function Get-ASDatabase 755 | { 756 | [CmdletBinding()] 757 | param 758 | ( 759 | [string] $serverName 760 | , 761 | [string] $databaseName 762 | , 763 | [string] $username 764 | , 765 | [string] $password 766 | , 767 | [string] $bimFilePath 768 | ) 769 | 770 | try 771 | { 772 | 773 | $server = Connect-ASServer -serverName $serverName -userId $username -password $password 774 | 775 | $database = $server.Databases.GetByName($databaseName) 776 | 777 | $databaseStr = [Microsoft.AnalysisServices.Tabular.JsonSerializer]::SerializeDatabase($database) 778 | 779 | $databaseStr | Out-File $bimFilePath -Force 780 | } 781 | finally 782 | { 783 | if ($server) 784 | { 785 | $server.Dispose() 786 | } 787 | } 788 | } 789 | 790 | Function Get-ASTablePartition 791 | { 792 | [CmdletBinding()] 793 | param 794 | ( 795 | [string] $serverName 796 | , 797 | [string] $databaseName 798 | , 799 | [string] $tableName 800 | , 801 | [string] $username 802 | , 803 | [string] $password 804 | ) 805 | 806 | try 807 | { 808 | 809 | $server = Connect-ASServer -serverName $serverName -userId $username -password $password 810 | 811 | $database = $server.Databases.GetByName($databaseName) 812 | 813 | $table = $database.Model.Tables.Find($tableName) 814 | 815 | if ($table -eq $null) 816 | { 817 | throw "Cannot find table '$tableName'" 818 | } 819 | 820 | Write-Log "Getting partitions for table '$tableName'" 821 | 822 | $table.Partitions |% { 823 | 824 | $partition = $_ 825 | 826 | $outputObj = @{ 827 | "Name" = $partition.Name 828 | ; 829 | "RefreshedTime" = $partition.RefreshedTime 830 | ; 831 | "State" = $partition.State 832 | ; 833 | "Type" = $partition.SourceType.ToString() 834 | } 835 | 836 | if ($partition.SourceType -eq [Microsoft.AnalysisServices.Tabular.PartitionSourceType]::Query) 837 | { 838 | $outputObj.Query = $partition.Source.Query 839 | } 840 | else 841 | { 842 | $outputObj.Query = $partition.Source.Expression 843 | } 844 | 845 | Write-Output $outputObj 846 | } 847 | 848 | } 849 | finally 850 | { 851 | if ($server) 852 | { 853 | $server.Dispose() 854 | } 855 | } 856 | } 857 | 858 | Function Update-ASDatabase 859 | { 860 | param 861 | ( 862 | [string]$serverName 863 | , 864 | [string]$databaseName 865 | , 866 | [string]$bimFilePath 867 | , 868 | [string]$outputFile 869 | , 870 | [switch]$deployToServer = $true 871 | , 872 | [switch]$deployRoles = $false 873 | , 874 | [switch]$deployConnections = $false 875 | , 876 | [switch]$deployPartitions = $false 877 | , 878 | [switch]$deployPartitionsWithRefreshPolicy = $false 879 | , 880 | [hashtable]$dataSourceSettings 881 | , 882 | [hashtable]$parameters 883 | , 884 | [string] $username 885 | , 886 | [string] $password 887 | , 888 | [string] $authToken 889 | , 890 | [System.Text.Encoding] $encoding = [System.Text.Encoding]::Default 891 | ) 892 | 893 | try 894 | { 895 | 896 | Write-Log "Starting AS Deploy of '$databaseName' on '$serverName' from bimfile: '$bimFilePath'" 897 | 898 | $model = [IO.File]::ReadAllText($bimFilePath, $encoding) 899 | 900 | $database = [Microsoft.AnalysisServices.Tabular.JsonSerializer]::DeserializeDatabase($model) 901 | 902 | $database.ID = $databaseName 903 | $database.Name = $databaseName 904 | 905 | if ($parameters) 906 | { 907 | $parameters.GetEnumerator() |? { 908 | 909 | $parameterName = $_.Name 910 | $parameterValue = $_.Value 911 | 912 | $bimParameters = @($database.model.Expressions |? { $_.Name -eq $parameterName }) 913 | 914 | if ($bimParameters.Count -gt 1) 915 | { 916 | throw "Found more that one parameter with name '$parameterName'" 917 | } 918 | 919 | if ($bimParameters.Count -eq 1) 920 | { 921 | Write-Log "Updating parameter '$parameterName' with value '$parameterValue'" 922 | 923 | $bimParameters[0].Expression = $bimParameters[0].Expression -replace """?(.*)""? meta","""$parameterValue"" meta" 924 | } 925 | else 926 | { 927 | Write-Log "Cannot find parameter '$parameterName' in bimfile" 928 | } 929 | } 930 | 931 | # TODO - Check Schema after change 932 | } 933 | 934 | $tmslScript = [Microsoft.AnalysisServices.Tabular.JsonScripter]::ScriptCreateOrReplace($database, $true) 935 | 936 | $tmslObj = [Newtonsoft.Json.Linq.JObject]::Parse($tmslScript) 937 | 938 | $tmslModel = $tmslObj["createOrReplace"]["database"]["model"] 939 | 940 | $server = Connect-ASServer -serverName $serverName -userId $username -password $password -authToken $authToken 941 | 942 | $currentDatabase = $server.Databases.FindByName($databaseName) 943 | 944 | if ($currentDatabase -ne $null) 945 | { 946 | $tmslObj["createOrReplace"]["object"]["database"].Value = $currentDatabase.Name 947 | 948 | $tmslObj["createOrReplace"]["database"].Add("id", [Newtonsoft.Json.Linq.JValue]::CreateString($currentDatabase.ID)) 949 | 950 | $tmslObj["createOrReplace"]["database"]["name"].Value = $currentDatabase.Name 951 | 952 | # if !deployRoles then remove the ones on bim and add the existing ones 953 | 954 | if (!$deployRoles) 955 | { 956 | Write-Log "Keeping roles in the current DB" 957 | 958 | $rolesArray = [Newtonsoft.Json.Linq.JArray]::new() 959 | 960 | foreach($role in $currentDatabase.Model.Roles) 961 | { 962 | $json = [Microsoft.AnalysisServices.Tabular.JsonSerializer]::SerializeObject($role) 963 | 964 | $rolesArray.Add([Newtonsoft.Json.Linq.JObject]::Parse($json)) 965 | } 966 | 967 | if (!$tmslModel["roles"]) 968 | { 969 | $tmslModel.Add("roles", $rolesArray) 970 | } 971 | else 972 | { 973 | $tmslModel["roles"] = $rolesArray 974 | } 975 | 976 | } 977 | 978 | if (!$deployConnections) 979 | { 980 | Write-Log "Keeping the current connections" 981 | 982 | if ($currentDatabase.Model.DataSources -and $currentDatabase.Model.DataSources.Count -ne 0) 983 | { 984 | $dataSourcesArray = [Newtonsoft.Json.Linq.JArray]::new() 985 | 986 | foreach($dataSource in $currentDatabase.Model.DataSources) 987 | { 988 | $json = [Microsoft.AnalysisServices.Tabular.JsonSerializer]::SerializeObject($dataSource) 989 | 990 | $dataSourcesArray.Add([Newtonsoft.Json.Linq.JObject]::Parse($json)) 991 | } 992 | 993 | if (!$tmslModel["dataSources"]) 994 | { 995 | $tmslModel.Add("dataSources", $dataSourcesArray) 996 | } 997 | else 998 | { 999 | $tmslModel["dataSources"] = $dataSourcesArray 1000 | } 1001 | } 1002 | } 1003 | 1004 | 1005 | # Partitions 1006 | 1007 | foreach($table in $tmslModel["tables"]) 1008 | { 1009 | $tableName = $table["name"].Value 1010 | 1011 | if ($currentDatabase.Model.Tables.Contains($tableName)) 1012 | { 1013 | $currentTable = $currentDatabase.Model.Tables[$tableName] 1014 | 1015 | # Keeps database partitions only if is asked to or the table as a refresh policy (incremental refresh) 1016 | 1017 | if (!$deployPartitions -or ($currentTable.RefreshPolicy -and !$deployPartitionsWithRefreshPolicy)) 1018 | { 1019 | if ($currentTable.RefreshPolicy) 1020 | { 1021 | Write-Log "Keeping the existent partitions on table '$tableName' because it has a refreshpolicy" 1022 | } 1023 | else 1024 | { 1025 | Write-Log "Keeping the existent partitions on table '$tableName'" 1026 | } 1027 | 1028 | $partitionsArray = [Newtonsoft.Json.Linq.JArray]::new() 1029 | 1030 | foreach($partition in $currentTable.Partitions) 1031 | { 1032 | $json = [Microsoft.AnalysisServices.Tabular.JsonSerializer]::SerializeObject($partition) 1033 | 1034 | $partitionsArray.Add([Newtonsoft.Json.Linq.JObject]::Parse($json)) 1035 | } 1036 | 1037 | $table["partitions"] = $partitionsArray 1038 | 1039 | } 1040 | else 1041 | { 1042 | Write-Log "Overriding partitions on table '$tableName'" 1043 | } 1044 | 1045 | } 1046 | } 1047 | 1048 | } 1049 | 1050 | if ($dataSourceSettings) 1051 | { 1052 | Write-Log "Overriding DataSource settings" 1053 | 1054 | foreach($jsonObj in $tmslModel["dataSources"]) 1055 | { 1056 | $dataSourceSettings.Keys |% { 1057 | 1058 | $dsName = $_ 1059 | 1060 | # If the DataSource name matches apply the configuration 1061 | 1062 | if ($jsonObj["name"].Value -eq $dsName) 1063 | { 1064 | $dsObj = $dataSourceSettings[$dsName] 1065 | 1066 | if ($dsObj.ContainsKey("CredentialPassword")) 1067 | { 1068 | Write-Log "Setting CredentialPassword for datasource '$dsName'" 1069 | 1070 | $jsonObj["credential"].Add("Password", [Newtonsoft.Json.Linq.JValue]::CreateString($dsObj["CredentialPassword"])) 1071 | } 1072 | 1073 | if ($dsObj.ContainsKey("CredentialUsername")) 1074 | { 1075 | Write-Log "Setting CredentialUsername for datasource '$dsName'" 1076 | 1077 | $jsonObj["credential"]["Username"] = [Newtonsoft.Json.Linq.JValue]::CreateString($dsObj["CredentialUsername"]) 1078 | } 1079 | 1080 | if ($dsObj.ContainsKey("ConnectionString")) 1081 | { 1082 | Write-Log "Setting ConnectionString for datasource '$dsName'" 1083 | 1084 | $jsonObj["connectionString"] = [Newtonsoft.Json.Linq.JValue]::CreateString($dsObj["ConnectionString"]) 1085 | } 1086 | 1087 | if ($dsObj.ContainsKey("Server")) 1088 | { 1089 | Write-Log "Setting server for datasource '$dsName'" 1090 | 1091 | $jsonObj["connectionDetails"]["address"]["server"] = [Newtonsoft.Json.Linq.JValue]::CreateString($dsObj["Server"]) 1092 | } 1093 | 1094 | if ($dsObj.ContainsKey("Database")) 1095 | { 1096 | Write-Log "Setting Database for datasource '$dsName'" 1097 | 1098 | $jsonObj["connectionDetails"]["address"]["database"] = [Newtonsoft.Json.Linq.JValue]::CreateString($dsObj["Database"]) 1099 | } 1100 | } 1101 | 1102 | } 1103 | } 1104 | } 1105 | 1106 | $deployTmslScript = $tmslObj.ToString() 1107 | 1108 | # Write XMLA to disk 1109 | 1110 | if (![string]::IsNullOrEmpty($outputFile)) 1111 | { 1112 | # ensure directory exists 1113 | 1114 | New-Item -Path $outputFile -ItemType File -Force -ErrorAction SilentlyContinue | Out-Null 1115 | 1116 | Write-Log "Writing output file: '$outputFile'" 1117 | 1118 | $deployTmslScript | Out-File $outputFile -Force 1119 | 1120 | } 1121 | 1122 | if ($deployToServer) 1123 | { 1124 | Write-Log "Deploying '$databaseName' on '$server'" 1125 | 1126 | $result = $server.Execute($deployTmslScript) 1127 | 1128 | if ($result.ContainsErrors -eq $true) 1129 | { 1130 | $strErrors = "" 1131 | 1132 | # Get Messages 1133 | 1134 | $nl = [System.Environment]::NewLine 1135 | 1136 | foreach($xr in $result.Messages) 1137 | { 1138 | $strErrors += $xr.Description 1139 | $strErrors += $nl 1140 | } 1141 | 1142 | throw "Error executing Deploy to Server: $($nl)$($strErrors)" 1143 | } 1144 | } 1145 | else 1146 | { 1147 | Write-Log "Skipping deploy to '$databaseName' on '$server'" 1148 | } 1149 | 1150 | } 1151 | finally 1152 | { 1153 | if ($currentDatabase) 1154 | { 1155 | $currentDatabase.Dispose() 1156 | } 1157 | 1158 | if ($server) 1159 | { 1160 | $server.Dispose() 1161 | } 1162 | } 1163 | } 1164 | 1165 | Function Connect-ASServer 1166 | { 1167 | [CmdletBinding()] 1168 | param 1169 | ( 1170 | [string]$serverName, 1171 | [string]$userId, 1172 | [string]$password, 1173 | [string]$authToken 1174 | ) 1175 | 1176 | Write-Log "Connecting to server '$serverName'" 1177 | 1178 | $server = New-Object Microsoft.AnalysisServices.Tabular.Server 1179 | 1180 | $connStr = "DataSource=$serverName" 1181 | 1182 | if (![string]::IsNullOrEmpty($authToken)) 1183 | { 1184 | $connStr += ";User ID=;Password=$authToken" 1185 | } 1186 | elseif (![string]::IsNullOrEmpty($userId) -and ![string]::IsNullOrEmpty($password)) 1187 | { 1188 | $connStr += ";User ID=$userId;Password=$password" 1189 | } 1190 | 1191 | $server.Connect($connStr) 1192 | 1193 | $server 1194 | 1195 | } 1196 | 1197 | 1198 | Function Save-ASDatabaseChanges 1199 | { 1200 | [CmdletBinding()] 1201 | param 1202 | ( 1203 | [Microsoft.AnalysisServices.Tabular.Database]$database 1204 | , [int] $maxParallelism = 5 1205 | , [Microsoft.AnalysisServices.Tabular.SaveFlags] $flags = [Microsoft.AnalysisServices.Tabular.SaveFlags]::Default 1206 | ) 1207 | 1208 | if ($database.Model.HasLocalChanges) 1209 | { 1210 | Write-Log "Saving Changes with MaxParallelism of '$maxParallelism'" 1211 | 1212 | $saveOptions = New-Object "Microsoft.AnalysisServices.Tabular.SaveOptions" 1213 | $saveOptions.MaxParallelism = $maxParallelism 1214 | $saveOptions.SaveFlags = $flags 1215 | 1216 | $database.Model.SaveChanges($saveOptions) 1217 | 1218 | # Using Reflection because of the error 'invalid type for a default value': https://social.technet.microsoft.com/Forums/azure/en-US/607674fa-9f67-4750-8584-5c396d58460d/trying-process-tabular-1400-amo-powershell?forum=sqlanalysisservices 1219 | 1220 | #$method = $asModelType.GetMethod("SaveChanges", ([Reflection.BindingFlags] "Public,Instance"), $null, [Reflection.CallingConventions]::Any, @([Microsoft.AnalysisServices.Tabular.SaveOptions]), $null) 1221 | 1222 | #$method.Invoke($database.Model, $saveOptions) 1223 | } 1224 | else 1225 | { 1226 | Write-Log "No changes on database $($database.Name)" 1227 | } 1228 | } 1229 | 1230 | 1231 | Function Undo-ASDatabaseChanges 1232 | { 1233 | [CmdletBinding()] 1234 | param 1235 | ( 1236 | [Microsoft.AnalysisServices.Tabular.Database]$database 1237 | ) 1238 | 1239 | try 1240 | { 1241 | if ($database.Model.HasLocalChanges) 1242 | { 1243 | Write-Log "Undo Changes" 1244 | 1245 | $database.Model.UndoLocalChanges() 1246 | 1247 | #$method = $asModelType.GetMethod("UndoLocalChanges", ([Reflection.BindingFlags] "Public,Instance"), $null, [Reflection.CallingConventions]::Any, @(), $null) 1248 | 1249 | #$method.Invoke($database.Model) 1250 | } 1251 | else 1252 | { 1253 | Write-Log "No changes on database $($database.Name)" 1254 | } 1255 | } 1256 | catch 1257 | { 1258 | Write-Log "Error on UndoLocalChanges(): $($_.Exception.Message)" -Level Error 1259 | } 1260 | 1261 | 1262 | } 1263 | 1264 | function Write-Log 1265 | { 1266 | [CmdletBinding()] 1267 | Param( 1268 | [Parameter(Mandatory=$true)][string]$Message 1269 | , [Parameter(Mandatory=$false)][ValidateSet("Log", "Warning", "Error", "Debug")] [string]$Level = "Debug" 1270 | ) 1271 | 1272 | try 1273 | { 1274 | # Format the message 1275 | 1276 | $now = [datetime]::UtcNow 1277 | 1278 | $formattedMessage = "$($now.ToString(""yyyy-MM-dd HH:mm:ss"")) | $Message" 1279 | 1280 | if($script:AzureDevOpsLogs) 1281 | { 1282 | $devopsLog = "debug" 1283 | 1284 | if ($Level -in @("Error")) 1285 | { 1286 | $devopsLog = "error" 1287 | } 1288 | elseif($Level -in @("Warning")) 1289 | { 1290 | $devopsLog = "Warning" 1291 | } 1292 | 1293 | $formattedMessage = "##[$devopsLog] $formattedMessage" 1294 | } 1295 | else 1296 | { 1297 | $formattedMessage = "$Level | $formattedMessage" 1298 | } 1299 | 1300 | if ($Level -eq "Error") 1301 | { 1302 | Write-Host $formattedMessage -ForegroundColor Red 1303 | } 1304 | elseif($Level -eq "Warning") 1305 | { 1306 | Write-Host $formattedMessage -ForegroundColor Yellow 1307 | } 1308 | else 1309 | { 1310 | Write-Host $formattedMessage 1311 | } 1312 | } 1313 | catch 1314 | { 1315 | Write-Host ("Error Writing to Log: '{0}'" -f $_.ToString()) 1316 | } 1317 | } --------------------------------------------------------------------------------