pbiTools.psm1
# Power BI Tools and Library # (v) Bob, Prodata Sept 2019 # History # 24/09/2019 First Version # # Install-Module MicrosoftPowerBIMgmt function New-pbiReport { param( [Parameter(Mandatory=$True)]$Path, #File OR Folder. Can be Array [Parameter(Mandatory=$True)]$Workspace, #Single Workspace [Parameter(Mandatory=$False)]$Name, #Name of DataSet/Report. If Blank will take from File [Parameter(Mandatory=$False)]$SqlServer, #Name of SQL Servr to Map Model to (If a Model ) [Parameter(Mandatory=$False)]$UserName #Name of Current User. If specified will take over dataset if needed ) ) $ErrorActionPreference = "Stop" $WorkspaceID = Get-PowerBIWorkspace -Name $Workspace $BaseName =$Name foreach ($p in $Path) { Get-ChildItem -Path $p| Where {$_.extension -like ".pbix"}| foreach { Write-Verbose -Message $_ if (!($BaseName)) {$Name=$_.BaseName } else {$Name =$BaseName} $Id= New-PowerBIReport -Path $_.FullName -Name $Name -Workspace $WorkspaceID -ConflictAction:"CreateOrOverwrite" | Select -ExpandProperty "Id" $count =@(Get-PowerBIReport -Workspace $WorkspaceID -Name $_.BaseName ).Count # if ($count -gt 1) { # Invoke-pbiReportBindFix -ReportId $Id # } if ($SqlServer) { Set-pbiSqlServer -Workspace $Workspace -Name $Name -SqlServer $SqlServer -UserName $UserName } else { Set-pbiDatasetGateway -Workspace $Workspace -Dataset $Name } } } } #Check if Linked Report has Duplicate and fix by # # function Invoke-pbiReportBindFix { } function Check-pbiModules { Load-Module "SqlServer" Load-Module "MicrosoftPowerBIMgmt" } function Remove-pbiReport { param( [Parameter(Mandatory=$True)]$Workspace #Single Workspace ) $WorkspaceId=(Get-PowerBIWorkspace -Name $Workspace) | Select -ExpandProperty "Id" Get-PowerBIReport -Workspace (Get-PowerBIWorkspace -Name $Workspace) | foreach { $Id =$_.Id $Url = "groups/$WorkspaceId/reports/$Id" Invoke-PowerBIRestMethod -Url $Url -Method Delete } Get-PowerBIDataSet -Workspace (Get-PowerBIWorkspace -Name $Workspace) | foreach { $Id =$_.Id $Url = "groups/$WorkspaceId/datasets/$Id" Invoke-PowerBIRestMethod -Url $Url -Method Delete } } function Set-pbiRebindReport { param( [Parameter(Mandatory=$True)][object]$Workspace , #Single Workspace [Parameter(Mandatory=$False)][String]$Report="*", #Name of Report. Can be * [Parameter(Mandatory=$False)][String]$ForceDatasetId #DataSetId to force a Bind To ) $ErrorActionPreference = "Stop" $WorkspaceId=(Get-PowerBIWorkspace -Name $Workspace) | Select -ExpandProperty "Id" Get-PowerBIReport -WorkspaceId $WorkspaceId | Where {$_.Name -like $Report} | foreach { $DatasetId=$_.DatasetId $reportId=$_.Id $Url ="groups/$WorkspaceId/datasets/$DatasetId" $Id=Get-PowerBIDataset -WorkspaceId $WorkspaceId -Id $DatasetId | Select -ExpandProperty "Id" if (!$Id) { Write-Verbose -Message ("Rebind Report: " + $_.Name) #Get Name of Dataset in Remote Workspace $Url ="groups/$WorkspaceId/datasets/$DatasetId" $json=Invoke-PowerBIRestMethod -Url $Url -Method Get $data = ConvertFrom-Json $json $DatasetName =$data.name if ($ForceDatasetId) { $newDatasetId=$ForceDatasetId } else { #Get Dataset ID for Local Workspace Dataset $newDatasetId=Get-PowerBIDataset -WorkspaceId $WorkspaceId | Where {$_.Name -eq $DatasetName } | Select -ExpandProperty "Id" } #Rebind Report to Dataset specified $url="groups/$WorkspaceId/reports/$reportId/Rebind" $Body=@" {datasetId: "$newDatasetId"} "@ Invoke-PowerBIRestMethod -Url $Url -Method Post -Body $Body } } } function Set-pbiDatasetTakeOver { param( [Parameter(Mandatory=$True)]$Workspace , #Single Workspace [Parameter(Mandatory=$False)]$Dataset="*", #Name of Dataset. Can by Array [Parameter(Mandatory=$False)]$UserName="" #Username of USer to Change Ownership to ) $ErrorActionPreference = "Stop" $WorkspaceId=(Get-PowerBIWorkspace -Name $Workspace) | Select -ExpandProperty "Id" Get-PowerBIDataset -WorkspaceId $WorkspaceId | Where {$_.Name -like $Dataset} |foreach { $Id =$_.Id $Url ="groups/$WorkspaceId/datasets/$Id/Default.TakeOver" Invoke-PowerBIRestMethod -Url $Url -Method Post -Body "" } } function Set-pbiDatasetGateway { param( [Parameter(Mandatory=$True)]$Workspace , #Single Workspace [Parameter(Mandatory=$False)]$Dataset="*" #Name of Dataset. Can by Array ) $ErrorActionPreference = "Stop" $WorkspaceId=(Get-PowerBIWorkspace -Name $Workspace) | Select -ExpandProperty "Id" Get-PowerBIDataset -WorkspaceId $WorkspaceId | Where {$_.Name -like $Dataset} |foreach { $Id =$_.Id $DataSet = $_.Name $IsOnPremGatewayRequired =$_.IsOnPremGatewayRequired if ($IsOnPremGatewayRequired) { $Url ="groups/$WorkspaceId/datasets/$Id/DiscoverGateways" $json=Invoke-PowerBIRestMethod -Url $Url -Method Get $data = ConvertFrom-Json $json $GatewayId=$data.value.Id $Body=@" {"gatewayObjectId": "$GatewayId"} "@ $url="groups/$WorkspaceId/datasets/$Id/BindToGateway" Write-Verbose -Message $url try { Invoke-PowerBIRestMethod -Url $Url -Method Post -Body $Body } catch [System.Net.Http.HttpRequestException],[AggregateException] { write-Error ("Could Not Bind Gateway on DataSet $DataSet . Check SqlServer Name and Gateway Configuration.") write-Error (Resolve-PowerBIError -Last | Out-String) } catch { #write-Error ( $_.Exception) write-Error (Resolve-PowerBIError -Last | Out-String) } } } } function Get-pbiStatus { param( [Parameter(Mandatory=$True)] [object] $Workspace , #WorkSpaces. Can be Array [Parameter(Mandatory=$False)][string] $Dataset="*",#Name of Dataset. Can by WildCard [Parameter(Mandatory=$False)][hashtable] $Errors =@{}, #Hash Table of Errors During Processing to tag them on Status returned [Parameter(Mandatory=$False)][hashtable] $StartTimes =@{} #Hash Table of Start Times if Processing Failures not logged ) # $ErrorActionPreference = "Stop" $DataSetList=@() ForEach ($w in $Workspace ) { $WorkspaceId=(Get-PowerBIWorkspace -Name $w) | Select -ExpandProperty "Id" Get-PowerBIDataset -WorkspaceId $WorkspaceId | Where {$_.Name -like $Dataset -and $_.IsRefreshable -eq $True} | Where-Object {$_.Name -NotIn "Usage Metrics Report"}|foreach { $Id=$_.Id $obj=$null $obj = New-Object System.Object $obj | Add-Member -type NoteProperty -Name WorkSpace -Value $w $obj | Add-Member -type NoteProperty -Name DataSetId -Value $_.id $obj | Add-Member -type NoteProperty -Name DataSet -Value $_.Name $obj | Add-Member -type NoteProperty -Name ConfiguredBy $_.ConfiguredBy $obj | Add-Member -type NoteProperty -Name IsOnPremGatewayRequired $_.IsOnPremGatewayRequired #Get last Refresh History $url="groups/$WorkspaceId/datasets/$Id/refreshes?" + '$top=1' $json=Invoke-PowerBIRestMethod -Url $Url -Method Get $data = ConvertFrom-Json $json $status =(&{if ($data.value.status -eq "Unknown") {"InProgress"} else {$data.value.status}}) if ($Errors.item($Id).length -gt 0) {$status = "Failed" } if ($Errors.item($Id).length -gt 0) { $startTime = $StartTimes.item($Id) $endTime =$startTime } else { if ($data.value.startTime) {$startTime = [datetime]$data.value.startTime} if ($data.value.EndTime){$endTime =[datetime]$data.value.EndTime} } $obj | Add-Member -type NoteProperty -Name refreshStatus $status $obj | Add-Member -type NoteProperty -Name refreshType $data.value.refreshType $obj | Add-Member -type NoteProperty -Name startTime $startTime $obj | Add-Member -type NoteProperty -Name EndTime $endTime $obj | Add-Member -type NoteProperty -Name Error ($Errors.item($Id) + $data.value.serviceExceptionJson ) $DataSetList+=$obj } } $DataSetList } function Invoke-pbiRefresh { param( [Parameter(Mandatory=$True)] [object]$Workspace , #Array of Workspaces [Parameter(Mandatory=$False)] [String]$Dataset="*", #Name of Dataset. Can by Array [parameter(Mandatory=$false)] [switch]$MailOnFailure=$False, #Email on Failure [parameter(Mandatory=$false)] [switch]$Wait=$True, #Wait for Failure (Synchronous) [parameter(Mandatory=$false)] [int]$WaitTime=10, #Time to wait for checking Processing Progress [parameter(Mandatory=$false)] [String]$SqlServer ,#Sql Server to Log Results. Will Log if this is present [parameter(Mandatory=$false)] [String]$SqlDatabaseName , #Sql Server to Log Results to. Will Log if this is present [parameter(Mandatory=$false)] [String]$SqlSchemaName ="dbo" , #Sql Server to Log Schema to. Will Log if this is present [parameter(Mandatory=$false)] [String]$SqlTableName ="ProcessView", #Sql Table to Log Results to [parameter(Mandatory=$false)] [switch]$IgnoreProcessError, #Raise Error if any Process Errors. Default is to Fail on any Error [parameter(Mandatory=$false)] [switch]$NoOutput , #Output Status When Finished [parameter(Mandatory=$false)] [int]$DOP =4, #Degree of Parrallelism [parameter(Mandatory=$false)] [bool]$RetryFailed = 0, #Retry failed only dataset flag [parameter(Mandatory=$false)] [int]$RetryAttempts = 1, #No of retry attempts [parameter(Mandatory=$false)] [int]$RetryWaitTime = 20 #Time to wait in Seconds ) $ErrorActionPreference = "Stop" $errormessage="" Check-pbiModules $Datasets =@() $RefreshList =@() $index =0 $i=0 $Errors =@{} $StartTimes =@{} [string]$status="" ForEach ($w in $Workspace) { $WorkspaceId=(Get-PowerBIWorkspace -Name $w) | Select -ExpandProperty "Id" $sDatasets=Get-PowerBIDataset -WorkspaceId $WorkspaceId | Where {$_.Name -like $Dataset -and $_.IsRefreshable -eq $True } | Where-Object {$_.Name -NotIn "Usage Metrics Report"} forEach ($d in $sDatasets) { $AddDataset = $True If ($RetryFailed) { $status = (Get-pbiStatus -Workspace $w -Dataset $d.name) | Select -ExpandProperty "refreshStatus" If ($status -eq "Completed") {$AddDataset=$False} } if($AddDataset) { $record= New-Object System.Object $record | Add-Member -MemberType NoteProperty -Name "Index" -Value $i $record | Add-Member -MemberType NoteProperty -Name "DataSet" -Value $d.Name $record | Add-Member -MemberType NoteProperty -Name "DataSetId" -Value $d.Id $record | Add-Member -MemberType NoteProperty -Name "WorkSpace" -Value $w $record | Add-Member -MemberType NoteProperty -Name "WorkSpaceid" -Value $WorkspaceId $record | Add-Member -MemberType NoteProperty -Name "status" -Value "Running" $record | Add-Member -MemberType NoteProperty -Name "ProcessKey" -Value 0 $record | Add-Member -MemberType NoteProperty -Name "Error" -Value "" $record | Add-Member -MemberType NoteProperty -Name "startTime" -value $null $i+=1 $Datasets+=$record } } } $DatasetCount =$i $index=0 $Run=0 while ($index -lt $DataSetCount) { $Run+=1 $RefreshList =@() for ($i=1; $i -le $DOP; $i++) { if ($index -lt $DataSetCount ) { $w = $Datasets[$Index].WorkSpace $d = $Datasets[$Index].DataSet $status="" $ProcessKey= Invoke-pbiRefreshDataSet -Workspace $w -Dataset $d -SqlServer $SqlServer -SqlDatabaseName $SqlDatabaseName -SqlSchemaName $SqlSchemaName -NoWait $Datasets[$Index].ProcessKey=$ProcessKey if ($SqlServer ) { $Query ="select refreshStatus FROM {0}.{1}.{2} WHERE ProcessKey = {3}" -f $SqlDatabaseName,$SqlSchemaName, $sqlTableName, $ProcessKey Write-Verbose $Query $status= (Invoke-sqlcmd -ServerInstance $SqlServer -Query $Query).refreshStatus $Datasets[$Index].status = $status $Datasets[$Index].startTime =Get-Date if ($status -eq "Failed" ) { $Query ="select Error FROM {0}.{1}.{2} WHERE ProcessKey = {3}" -f $SqlDatabaseName,$SqlSchemaName, $sqlTableName, $ProcessKey $Datasets[$Index].Error = (Invoke-sqlcmd -ServerInstance $SqlServer -Query $Query).Error } } if ($status -ne "Failed") {$RefreshList+=$Datasets[$Index]} $index +=1 } } if (($Wait -eq$True) -and ($RefreshList.length -gt 0)) { Start-Sleep -Seconds 1 $Complete=$False While (!($Complete)) { ForEach ($r in $RefreshList | Where-Object {$_.status -ne "Completed" -and $_.status -ne "Failed" } ) { $WorkspaceId =$r.WorkSpaceId $Id=$r.DataSetId $DataSetIndex=$r.index $d=$Datasets[$DataSetIndex] if (!($Errors -contains $Id)) { $url="groups/$WorkspaceId/datasets/$Id/refreshes?" + '$top=1' try { $json=Invoke-PowerBIRestMethod -Url $Url -Method Get $data = ConvertFrom-Json $json if ($data.value.EndTime){$EndTime = Get-Date -Date $data.value.EndTime -f "dd/MM/yyyy HH:mm:ss.fff"} [string]$status=$data.value.status if ($d.status -ne $status -and $status -ne "Failed") { $Query="UPDATE [{0}].[{1}].[{2}] SET refreshStatus='{4}' WHERE ProcessKey={3}" -f $SqlDatabaseName,$SqlSchemaName, $sqlTableName, $r.ProcessKey, $status invoke-sqlcmd -ServerInstance $SqlServer -Database $SqlDatabaseName -Query $Query } if ($status -eq "Failed" -and $d.status -ne "Failed" ) { $Error =$data.value.serviceExceptionJson -replace "'","''" $Datasets[$DataSetIndex].Error=$Error $Query="UPDATE [{0}].[{1}].[{2}] SET EndTime='{6}', refreshStatus='{3}' , Error='{4}' WHERE ProcessKey={5}" -f $SqlDatabaseName,$SqlSchemaName, $sqlTableName, $status, $Error , $r.ProcessKey, $EndTime invoke-sqlcmd -ServerInstance $SqlServer -Database $SqlDatabaseName -Query $Query write-Warning ("`r`nError in $($r.WorkSpace)\$($r.DataSet): $Error" ) } if ($status -eq "Completed" ) { $Error =$data.value.serviceExceptionJson -replace "'","''" $Query="UPDATE [{0}].[{1}].[{2}] SET EndTime='{5}', refreshStatus='{3}' , Error=null WHERE ProcessKey={4}" -f $SqlDatabaseName,$SqlSchemaName, $sqlTableName, $status, $r.ProcessKey, $EndTime invoke-sqlcmd -ServerInstance $SqlServer -Database $SqlDatabaseName -Query $Query } $Datasets[$DataSetIndex].status = $status } catch [System.AggregateException] { $Error =(Resolve-PowerBIError -Last ).Message $status ="Failed" $Datasets[$DataSetIndex].status=$status $Datasets[$DataSetIndex].Error += $Error if ($Errors.ContainsKey( $id)) {$Errors[$Id] =$Errors[$Id] + '.' + $Erro} else {$Errors.Add($Id,$Error) } $Query="UPDATE [{0}].[{1}].[{2}] SET EndTime=getdate(), refreshStatus='{3}' , Error='{4}' WHERE ProcessKey={5}" -f $SqlDatabaseName,$SqlSchemaName, $sqlTableName, $status, $Error , $r.ProcessKey invoke-sqlcmd -ServerInstance $SqlServer -Database $SqlDatabaseName -Query $Query write-Warning ("`r`nError in $($r.WorkSpace)\$($r.DataSet): $Error" ) } catch { $status ="Failed" $Datasets[$DataSetIndex].status=$status $Datasets[$DataSetIndex].Error += $Error if ($Errors.ContainsKey( $id)) {$Errors[$Id] =$Errors[$Id] + '.' + $Error} else {$Errors.Add($Id,$Error) } $Error =$Errors[$Id] $Error = $Error -replace "'","''" $Query="UPDATE [{0}].[{1}].[{2}] SET EndTime=getdate(), refreshStatus='{3}' , Error='{4}' WHERE ProcessKey={5}" -f $SqlDatabaseName,$SqlSchemaName, $sqlTableName, $status, $Error , $r.ProcessKey invoke-sqlcmd -ServerInstance $SqlServer -Database $SqlDatabaseName -Query $Query write-Warning ("`r`nError in $($r.WorkSpace)\$($r.DataSet): $Error" ) } } } #Check if all failed or Completed, otherwise Wait $Complete=$True $RefreshList | Where {$_.status -ne "Completed" -and $_.status -ne "Failed"} | ForEach-Object { $Complete=$False } if (!($Complete )) {Start-Sleep -Seconds $WaitTime} } } } #Retry to refresh Failed datasets $FailedDatasetsList = $Datasets | Where {$_.status -eq "Failed"} If ($RetryAttempts -gt 0 -and $FailedDatasetsList.Count -gt 0) { Write-Host ("Retrying Failed Datasets", $FailedDatasetsList.DataSet) Start-Sleep -Seconds $RetryWaitTime $RetryAttempts= $RetryAttempts-1 Invoke-pbiRefresh -Workspace $WorkSpace -Wait -SqlServer $SqlServer -SqlDatabaseName $SqlDatabaseName -SqlSchemaName "lineage" -SqlTableName "ProcessLog" -RetryAttempts $RetryAttempts -RetryFailed 1 } $ErrorActionPreference = "continue" ForEach ($d in $Datasets) { $checkStatus = (Get-pbiStatus -Workspace $d.Workspace -Dataset $d.DataSet) | Select -ExpandProperty "refreshStatus" If ($checkStatus -eq "Failed"){ if (!$Errors.ContainsKey( $d.DataSetId) ) {$Errors.Add($d.DataSetId, $d.Error)} $StartTimes.Add( $d.DataSetId, $d.startTime) if ($d.Error) {$failed=$True ; $LastError= $d.Error} } } if (!($NoOutput.IsPresent)) { Get-pbiStatus -Workspace $WorkSpace -Dataset $DataSet -Errors $Errors -StartTimes $StartTimes | ft } if ($failed) { EXIT 1 } } function Invoke-pbiRefreshDataSet { param( [Parameter(Mandatory=$True)] [object]$Workspace , #Workspace Name (Required) [Parameter(Mandatory=$False)] [String]$Dataset, #Name of Dataset. [Parameter(Mandatory=$False)] [String]$DatasetId, #Id of DataSet [parameter(Mandatory=$false)] [switch]$MailOnFailure=$False, #Email on Failure [parameter(Mandatory=$false)] [switch]$NoWait, #Wait for Failure (Synchronous) [parameter(Mandatory=$false)] [Int16]$WaitTime=5, #Secs to Wait before status Refresh [parameter(Mandatory=$false)] [String]$SqlServer , #Sql Server to Log Results. Will Log if this is present [parameter(Mandatory=$false)] [String]$SqlDatabaseName , #Sql Server to Log Results to. Will Log if this is present [parameter(Mandatory=$false)] [String]$SqlSchemaName ="lineage" , #Sql Server to Log Schema to. Will Log if this is present [parameter(Mandatory=$false)] [String]$SqlTableName ="ProcessLog" #Sql Table to Log Results to ) $ErrorActionPreference = "Stop" $failed=$False $WorkspaceId=(Get-PowerBIWorkspace -Name $Workspace) | Select -ExpandProperty "Id" if (-Not ($DataSetId)) { $d=Get-PowerBIDataset -WorkspaceId $WorkspaceId | Where {$_.Name -like $Dataset -and $_.IsRefreshable -eq $True } $DataSetId =$d.Id } else { $d=Get-PowerBIDataset -WorkspaceId $WorkspaceId -Id $DataSetId } $url="groups/$WorkspaceId/datasets/$DataSetId/refreshes?" + '$top=1' $json=Invoke-PowerBIRestMethod -Url $Url -Method Get $data = ConvertFrom-Json $json $Id =$d.Id $DatasetName=$d.Name $IsOnPremGatewayRequired =$d.IsOnPremGatewayRequired if ($data.Value.Status -ne "Unknown") { if ($MailOnFailure.IsPresent) {$notifyOption="MailOnFailure"} else { $notifyOption="NoNotification" } $Body='{"notifyOption": "' + $notifyOption+ '"}' $url="groups/$WorkspaceId/datasets/$Id/refreshes" Write-Host ("Refreshing {0}/{1}" -f $Workspace , $DatasetName) try { if ($SqlServer) { $Query="INSERT INTO [{0}].[{1}].[{2}] (Workspace,DataSetId,DataSet, IsOnPremGatewayRequired, refreshStatus, refreshType, startTime) VALUES ('{3}','{4}','{5}','{6}','{7}','{8}', getdate()); SELECT SCOPE_IDENTITY()" -f $SqlDatabaseName,$SqlSchemaName, $sqlTableName, $Workspace, $id,$DatasetName, $IsOnPremGatewayRequired, "Started","ViaApi" Write-Verbose $Query $Key= (invoke-sqlcmd -ServerInstance $SqlServer -Database $SqlDatabaseName -Query $Query)[0] } $res=Invoke-PowerBIRestMethod -Url $Url -Method Post -Body $Body if ($SqlServer) { $Query="UPDATE [{0}].[{1}].[{2}] SET refreshStatus='Started' WHERE ProcessKey={3}" -f $SqlDatabaseName,$SqlSchemaName, $sqlTableName, $key invoke-sqlcmd -ServerInstance $SqlServer -Database $SqlDatabaseName -Query $Query } } catch [AggregateException] { $failed=$True $error=$_.Exception.innerexception.message if ($error.Contains("400 (Bad Request)")) {$error ="Refresh Limit Reached, or API Failure. (Bad Request)" } write-Warning ("`r`nRefresh $w\$DatasetName. $error " ) if ($SqlServer) { $Query="UPDATE [{0}].[{1}].[{2}] SET EndTime=getdate(), refreshStatus='Failed', Error = '{3}' WHERE ProcessKey={4}" -f $SqlDatabaseName,$SqlSchemaName, $sqlTableName, $error, $key Write-Verbose $Query invoke-sqlcmd -ServerInstance $SqlServer -Database $SqlDatabaseName -Query $Query } } catch { $failed=$True write-Warning ("`r`nRefresh $w\$DatasetName. $_.Exception.message" ) Write-Verbose -Message $_.Exception $Error = $_.Exception -replace "'","''" if ($SqlServer) { $Query="UPDATE [{0}].[{1}].[{2}] SET EndTime=getdate(), refreshStatus='Failed', Error = '{3}' WHERE ProcessKey={4}" -f $SqlDatabaseName,$SqlSchemaName, $sqlTableName, $Error, $key invoke-sqlcmd -ServerInstance $SqlServer -Database $SqlDatabaseName -Query $Query } } } elseif($data.Value.Status -eq "Unknown"){ if ($SqlServer) { $Query="INSERT INTO [{0}].[{1}].[{2}] (Workspace,DataSetId,DataSet, IsOnPremGatewayRequired, refreshStatus, refreshType, startTime,endTime) VALUES ('{3}','{4}','{5}','{6}','{7}','{8}', getdate(),getdate()); SELECT SCOPE_IDENTITY()" -f $SqlDatabaseName,$SqlSchemaName, $sqlTableName, $Workspace, $id,$DatasetName, $IsOnPremGatewayRequired, "Unknown","ViaApi" Write-Verbose $Query $Key= (invoke-sqlcmd -ServerInstance $SqlServer -Database $SqlDatabaseName -Query $Query)[0] } } if (!($NoWait.IsPresent )) { $startTime=get-date $url="groups/$WorkspaceId/datasets/$Id/refreshes?" + '$top=1' While (!($Complete)) { $json=Invoke-PowerBIRestMethod -Url $Url -Method Get $data = ConvertFrom-Json $json $status=$data.value.status if ($status -eq "Failed" -or $status -eq "Completed") {$Complete =$True} else {Start-Sleep -Seconds $WaitTime} } if ($status -ne "Completed") { Write-Warning $data.value.serviceExceptionJson $Error =$data.value.serviceExceptionJson -replace "'","''" } $Query="UPDATE [{0}].[{1}].[{2}] SET EndTime=getdate(), refreshStatus='{3}' , Error='{4}' WHERE ProcessKey={5}" -f $SqlDatabaseName,$SqlSchemaName, $sqlTableName, $status, $Error , $key invoke-sqlcmd -ServerInstance $SqlServer -Database $SqlDatabaseName -Query $Query if ($status -ne "Completed") { write-Warning ("`r`nRefresh $w\$DatasetName. $Error" ) return $Key } } else { return $Key } } function Set-pbiSqlServer { param( [Parameter(Mandatory=$True)]$Workspace , #Single Workspace [Parameter(Mandatory=$False)]$Name="*", #Name of Dataset. Can by Array [Parameter(Mandatory=$True)]$SqlServer="", #New SqlServer Name [Parameter(Mandatory=$False)]$UserName="" #Username of validated user. If this is set then updaet will TakeOver Dataset if required ) $ErrorActionPreference = "Stop" $WorkspaceId=(Get-PowerBIWorkspace -Name $Workspace) | Select -ExpandProperty "Id" Get-PowerBIDataset -WorkspaceId $WorkspaceId | Where {$_.Name -like $Name} |foreach { $ConfiguredBy=$_.ConfiguredBy $Id =$_.Id $Dataset = $_.Name $Url = "groups/$WorkspaceId/datasets/$Id/datasources" $ds=Invoke-PowerBIRestMethod -Url $Url -Method Get $data = ConvertFrom-Json $ds $datasourceType=$data.value.datasourceType $oldServer =$data.value.connectionDetails.server $Database =$data.value.connectionDetails.database if ($Username) { if ($ConfiguredBy -ne $UserName) { Set-pbiDatasetTakeOver -Workspace $Workspace -Dataset $Dataset $ConfiguredBy=(Get-PowerBIDataset -WorkspaceId $WorkspaceId -Id $Id) | Select -ExpandProperty "ConfiguredBy" if ($ConfiguredBy -ne $UserName) { Write-Error "Cannot Updatedatasource. Owner=$ConfiguredBy,Current UserName=$UserName" } } } if ($datasourceType -eq "sql") { $Url = "groups/$WorkspaceId/datasets/$Id/updatedatasources" $Body=@" {"updateDetails":[ {"connectionDetails": { "server": "$SqlServer", "database": "$Database" }, "datasourceSelector": { "datasourceType": "Sql", "connectionDetails": { "server": "$OldServer", "database": "$Database" } } } ] } "@ Write-Verbose -Message $Url Write-Verbose -Message $Body Write-Host "Updating Datasource for $Workspace\$Dataset. Server $oldServer=>$SqlServer..." try { Invoke-PowerBIRestMethod -Url $Url -Method Post -Body $Body } catch [System.Net.Http.HttpRequestException],[AggregateException] { write-Error ("Update for $Workspace\$Dataset.PBIX Failed. Check PowerQuery has no MERGE operations. See https://docs.microsoft.com/en-us/rest/api/power-bi/datasets/updatedatasources") write-Error (Resolve-PowerBIError -Last | Out-String) } catch { write-Error ( $_.Exception) } } else { Write-Host "datasourceType=$datasourceType. Not Updated as not Sql" } #Set PBI Gateway (not Required as it now Auto Sets) Set-pbiDatasetGateway -Workspace $Workspace -Dataset $Dataset } } function Load-Module ($m) { # If module is imported say that and do nothing if (Get-Module | Where-Object {$_.Name -eq $m}) { return } else { # If module is not imported, but available on disk then import if (Get-Module -ListAvailable | Where-Object {$_.Name -eq $m}) { Import-Module $m } else { # If module is not imported, not available on disk, but is in online gallery then install and import if (Find-Module -Name $m | Where-Object {$_.Name -eq $m}) { Install-Module -Name $m -Force -Verbose -Scope CurrentUser -AllowClobber Import-Module $m } else { # If module is not imported, not available and not in online gallery then abort write-host "Module $m not imported, not available and not in online gallery, exiting." Exit 1 } } } } function Import-pbiAuditLogs { <# .SYNOPSIS Import PowerBI Audit Logs from Office 365 .DESCRIPTION Import PowerBI Audit Logs from Office 365. Used in conjunction with PBI Usage Metrics.PBIX Requires Global Admin Account and MSOnline POSH Module. This data is used to determine who has a valid PowerBI License so we can reporton % usage .PARAMETER Credential Powershell Credential. Must be Office 365 Global Admin. .PARAMETER SqlInstance SQL Server instance to Import to (default localhost) .PARAMETER Database SQL Database for Import .PARAMETER Schema SQL Schema. Default to dbo .PARAMETER Table SQL Table. Default to AuditLogs .PARAMETER SqlUsername SQL Username. If this is null will use windows auth for SQL .PARAMETER SqlPassword SQL Password. can be blank if using windows auth for SQL .EXAMPLE $username = "globaladmin@domain.ie" $password = "Plain Text Password" Import-pbiMsolUser -Username $username -password (ConvertTo-SecureString $password -AsPlainText -Force) -SqlInstance "AGDEV" -schema "dbo" -Database "TempDB" #> param( [parameter(Mandatory=$true)] [string]$Username, [parameter(Mandatory=$true)] [SecureString]$Password, [parameter(Mandatory=$false)] [string]$SqlInstance='localhost', [parameter(Mandatory=$false)] [string]$Database='TempDB', [parameter(Mandatory=$false)] [String]$Schema='dbo', [parameter(Mandatory=$false)] [String]$Table='AuditLog', [parameter(Mandatory=$false)] [String]$SqlUsername, [parameter(Mandatory=$false)] [String]$SqlPassword, [parameter(Mandatory=$false)] [Int]$ResultSize =5000 ) $ErrorActionPreference = "Stop" $Session= "" $Credential =New-Object System.Management.Automation.PSCredential ($Username, $Password) #Check if Module Loaded if (!(Get-Module -ListAvailable -Name "MSOnline")) { Write-Error "MSOnline Module Required. Run Install-Module MSOnline (as Administrator)" Exit 1 } if (!(Get-Module -ListAvailable -Name "SqlServer")) { Write-Error "MSOnline Module Required. Run Install-Module SqlServer (as Administrator)" Exit 1 } try { Write-Host ( "{0} Start Import-pbiAuditLogs" -f (get-date) ) #Get Watermark Value $SqlCmd="IF OBJECT_ID('[{0}].[{1}]') is not null SELECT MAX(CreationTime) as CreationTime FROM [{0}].[{1}]" -f $Schema, $Table Write-Verbose -Message $SqlCmd if ($SqlUsername) { $startDate=(Invoke-Sqlcmd -ServerInstance $SqlInstance -Database $Database -Query $SqlCmd -Username $sqlUsername -Password $SqlPassword) } else { $startDate=(Invoke-Sqlcmd -ServerInstance $SqlInstance -Database $Database -Query $SqlCmd) } if ($startDate) {[DateTime]$startDate=$startDate[0]; $startDate =$startDate.AddSeconds(1) } else { [DateTime]$startDate =(get-date).AddDays(-90) } $endDate=(get-date) Write-Host ( "{0} Start Import-pbiAuditLogs date range {1} to {2} " -f (get-date), $startDate, $endDate) Write-Verbose "Connecting to office365.com for AuditLogs. Please wait..." $Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri 'https://outlook.office365.com/powershell-liveid/' -Credential $Credential -Authentication Basic -AllowRedirection Import-PSSession $Session -DisableNameChecking -AllowClobber > $null #Get-PSSession | Remove-PSSession $scriptStart=(get-date) $sessionName = (get-date -Format 'u')+'pbiAuditLog' $i = 0 $currentResults = @() $rc Do { $currentResults=Search-UnifiedAuditLog -StartDate $startDate -EndDate $enddate -SessionId $sessionName -SessionCommand ReturnLargeSet -ResultSize $ResultSize -RecordType PowerBI | ` select-object -ExpandProperty AuditData | ForEach-Object {ConvertFrom-Json $_} | Select-Object -Property Id, RecordType, CreationTime, Operation, OrganizationId, UserType, UserId, userAgent, Activity, ConsumptionMethod, ItemName, WorkSpaceName, DashboardName, DatasetName, ReportName, WorkspaceId , ObjectId, DashboardId, DatasetId, ReportId if ($currentResults) { $aggregateResults += $currentResults if ($currentResults -is [array]) {$rc=$currentResults.Count } else {$rc=1} Write-Host ("Search-UnifiedAuditLog: Finished {3} search #{1}, {2} records: {0} min" -f [math]::Round((New-TimeSpan -Start $scriptStart).TotalMinutes,4), $i, $rc, $user.UserPrincipalName ) if ($currentResults.Count -lt $ResultSize) {$currentResults = @()} else {$i++} } else {$currentResults = @() } } Until ($currentResults.Count -eq 0) #Sort by CreationTime (for re-entrant behaviour) and then Insert to SQL if (!($aggregateResults)) { Write-Host ("{0} No new AuditLog Data" -f (Get-Date)) }else { Write-Host ("{0} Sorting and Write-SqlTableData to {1}" -f (Get-Date), $SqlInstance ) if ($SqlUsername) { $Credential =New-Object System.Management.Automation.PSCredential ($SqlUsername, (ConvertTo-SecureString $SqlPassword -AsPlainText -Force)) $aggregateResults | Sort-Object -Property CreationTime , Id | Write-SqlTableData -ServerInstance $SqlInstance -DatabaseName $Database -TableName $Table -SchemaName $Schema -Force -Credential $Credential } else { Write-SqlTableData -InputData $aggregateResults -ServerInstance $SqlInstance -DatabaseName $Database -TableName $Table -SchemaName $Schema -Force } } } catch { Write-Host $_.Exception.Message -ForegroundColor Red Write-Host $_.Exception.ErrorRecord.ScriptStackTrace -ForegroundColor Red Exit 1 } finally { #Cleanup. max Three Sessions allowed as per https://4sysops.com/forums/topic/office-365-you-have-exceeded-the-maximum-number-of-connections-allowed-3/ if ($Session) { Remove-PSSession -Id $Session.Id } Write-Host ( "{0} End Import-pbiAuditLogs" -f (get-date) ) } } function Import-pbiMsolUser { <# .SYNOPSIS Import User and License Data from O365. .DESCRIPTION Import User and License Data from O365.. Requires Global Admin Account and MSOnline POSH Module. This data is used to determine who has a valid PowerBI License so we can reporton % usage .PARAMETER Credential Powershell Credential. Must be Office 365 Global Admin. .PARAMETER SqlInstance SQL Server instance to Import to (default localhost) .PARAMETER Database SQL Database for Import .PARAMETER Schema SQL Schema. Default to dbo .PARAMETER Table SQL Table. Default to MsolUser .PARAMETER SqlUsername SQL Username. If this is null will use windows auth for SQL .PARAMETER SqlPassword SQL Password. can be blank if using windows auth for SQL .EXAMPLE $username = "globaladmin@domain.ie" $password = "Password" $secpasswd = ConvertTo-SecureString $password -AsPlainText -Force $Credential = New-Object System.Management.Automation.PSCredential ($username, $secpasswd) $Credential #> param( [parameter(Mandatory=$true)] [string]$Username, [parameter(Mandatory=$true)] [SecureString]$Password, [parameter(Mandatory=$false)] [string]$SqlInstance='localhost', [parameter(Mandatory=$false)] [string]$Database='TempDB', [parameter(Mandatory=$false)] [String]$Schema='dbo', [parameter(Mandatory=$false)] [String]$Table='MsolUser', [parameter(Mandatory=$false)] [String]$SqlUsername, [parameter(Mandatory=$false)] [String]$SqlPassword ) $Credential =New-Object System.Management.Automation.PSCredential ($Username, $Password) #Check if Module Loaded if (!(Get-Module -ListAvailable -Name "MSOnline")) { Write-Error "MSOnline Module Required. Run Install-Module MSOnline (as Administrator)" Exit 1 } if (!(Get-Module -ListAvailable -Name "SqlServer")) { Write-Error "MSOnline Module Required. Run Install-Module SqlServer (as Administrator)" Exit 1 } Connect-MsolService -Credential $Credential $SqlCmd="IF OBJECT_ID('[{0}].[{1}]') is not null DROP TABLE [{0}].[{1}]" -f $Schema, $Table Write-Verbose -Message $SqlCmd if ($SqlUsername) { Invoke-Sqlcmd -ServerInstance $SqlInstance -Database $Database -Query $SqlCmd -Username $sqlusername -Password $SqlPassword } else { Invoke-Sqlcmd -ServerInstance $SqlInstance -Database $Database -Query $SqlCmd } Get-MsolUser | Where-Object {($_.isLicensed -eq $true ) } | Select-Object -Property DisplayName, City, Country, Department, @{ name = "licenses"; expression = {ConvertTo-Json $_.licenses.accountskuid}}, ObjectId, State, UsageLocation, UserPrincipalName, @{ name="UserType"; expression = { [string]$_.UserType}}, @{ name="ValidationStatus"; expression= {[string]$_.ValidationStatus }}, WhenCreated ` | Write-SqlTableData -ServerInstance $SqlInstance -DatabaseName $Database -TableName $Table -SchemaName $Schema -Force } function Import-pbiAuditLogsRestApi { <# .SYNOPSIS Import PowerBI Audit Logs using Rest APIs .DESCRIPTION Import PowerBI Audit Logs using PBI rest apis. Used in conjunction with PBI Usage Metrics.PBIX Requires Global Admin Account and MSOnline POSH Module. This data is used to determine who has a valid PowerBI License so we can reporton % usage .PARAMETER Credential Powershell Credential. Must be Office 365 Global Admin. .PARAMETER SqlInstance SQL Server instance to Import to (default localhost) .PARAMETER Database SQL Database for Import .PARAMETER Schema SQL Schema. Default to dbo .PARAMETER Table SQL Table. Default to AuditLogs .PARAMETER SqlUsername SQL Username. If this is null will use windows auth for SQL .PARAMETER SqlPassword SQL Password. can be blank if using windows auth for SQL .EXAMPLE $username = "globaladmin@domain.ie" $password = "Plain Text Password" Import-pbiAuditLogsRestApi -Username $username -password (ConvertTo-SecureString $password -AsPlainText -Force) -SqlInstance "AGDEV" -schema "dbo" -Database "TempDB" #> param( [parameter(Mandatory=$true)] [string]$Username, [parameter(Mandatory=$true)] [SecureString]$Password, [parameter(Mandatory=$false)] [string]$SqlInstance='localhost', [parameter(Mandatory=$false)] [string]$Database='TempDB', [parameter(Mandatory=$false)] [String]$Schema='dbo', [parameter(Mandatory=$false)] [String]$Table='AuditLog', [parameter(Mandatory=$false)] [String]$SqlUsername, [parameter(Mandatory=$false)] [String]$SqlPassword ) $ErrorActionPreference = "Stop" $Session= "" $aggregateResults = @() $Credential =New-Object System.Management.Automation.PSCredential ($Username, $Password) Login-PowerBI -Credential $Credential #Check if Module Loaded if (!(Get-Module -ListAvailable -Name "MSOnline")) { Write-Error "MSOnline Module Required. Run Install-Module MSOnline (as Administrator)" Exit 1 } if (!(Get-Module -ListAvailable -Name "SqlServer")) { Write-Error "MSOnline Module Required. Run Install-Module SqlServer (as Administrator)" Exit 1 } try { Write-Host ( "{0} Start Import-pbiAuditLogs" -f (get-date) ) #$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri 'https://outlook.office365.com/powershell-liveid/' -Credential $Credential -Authentication Basic -AllowRedirection #Import-PSSession $Session -DisableNameChecking -AllowClobber > $null #Get-PSSession | Remove-PSSession #Get Watermark Value $SqlCmd="IF OBJECT_ID('[{0}].[{1}]') is not null SELECT MAX(CreationTime) as CreationTime FROM [{0}].[{1}]" -f $Schema, $Table Write-Verbose -Message $SqlCmd if ($SqlUsername) { $startDate=(Invoke-Sqlcmd -ServerInstance $SqlInstance -Database $Database -Query $SqlCmd -Username $sqlUsername -Password $SqlPassword) } else { $startDate=(Invoke-Sqlcmd -ServerInstance $SqlInstance -Database $Database -Query $SqlCmd) } if ($startDate) {[DateTime]$startDate=$startDate[0]; $startDate =$startDate.AddSeconds(1) } else { [DateTime]$startDate =(get-date).AddDays(-30) } $endDate=(get-date) Write-Host ( "{0} Start Import-pbiAuditLogs date range {1} to {2} " -f (get-date), $startDate, $endDate) $sessionName = (get-date -Format 'u')+'pbiAuditLog' #Download Data while ($startDate -le $endDate) { $StartDateTime = (Get-Date -Date ($startDate) -Format yyyy-MM-ddTHH:mm:ss) $EndDateTime = (Get-Date -Date ((($startDate.Date).AddDays(1)).AddSeconds(-1)) -Format yyyy-MM-ddTHH:mm:ss) Write-Host ( "{0} Downloading data from {1} to {2} " -f (get-date), $StartDateTime, $EndDateTIme) $aggregateResults += Get-PowerBIActivityEvent -StartDateTime $StartDateTime -EndDateTime $EndDateTime -ResultType JsonString | ForEach-Object {ConvertFrom-Json $_} | Select-Object -ExpandProperty Syncroot | Select-Object -Property Id, RecordType, CreationTime, Operation, OrganizationId, UserType, UserId, userAgent, Activity, ConsumptionMethod, ItemName, WorkSpaceName, DashboardName, DatasetName, ReportName, WorkspaceId , ObjectId, DashboardId, DatasetId, ReportId $startDate = $startDate.Date.AddDays(1) } #Sort by CreationTime (for re-entrant behaviour) and then Insert to SQL if (!($aggregateResults)) { Write-Host ("{0} No new AuditLog Data" -f (Get-Date)) }else { Write-Host ("{0} Sorting and Write-SqlTableData to {1}" -f (Get-Date), $SqlInstance ) if ($SqlUsername) { $Credential =New-Object System.Management.Automation.PSCredential ($SqlUsername, (ConvertTo-SecureString $SqlPassword -AsPlainText -Force)) $aggregateResults | Sort-Object -Property CreationTime , Id | Write-SqlTableData -ServerInstance $SqlInstance -DatabaseName $Database -TableName $Table -SchemaName $Schema -Force -Credential $Credential } else { Write-SqlTableData -InputData $aggregateResults -ServerInstance $SqlInstance -DatabaseName $Database -TableName $Table -SchemaName $Schema -Force } } } catch { Write-Host $_.Exception.Message -ForegroundColor Red Write-Host $_.Exception.ErrorRecord.ScriptStackTrace -ForegroundColor Red Exit 1 } finally { #Cleanup. max Three Sessions allowed as per https://4sysops.com/forums/topic/office-365-you-have-exceeded-the-maximum-number-of-connections-allowed-3/ #if ($Session) { Remove-PSSession -Id $Session.Id } Write-Host ( "{0} End Import-pbiAuditLogs" -f (get-date) ) } } function Remove-pbiBackupBlob { Param( [parameter(Mandatory=$true)] [string]$StorageAccountName, [parameter(Mandatory=$true)] [string]$SAS, [parameter(Mandatory=$false)] [object]$Containers, [parameter(Mandatory=$true)] [int]$Cleanup =7 ) $ctx=New-AzStorageContext -StorageAccountName $StorageAccountName -SasToken $SAS -ErrorAction Stop Write-Host $("Connecting to {0}" -f $StorageAccountName) foreach ($container in $Containers) { $blobs =Get-AzStorageBlob -Context $ctx -Container $Containers | Where-Object {$_.Name -like "*.pbix"} if ($blobs ){ foreach ($blob in $blobs) { $LastModified=$blob.LastModified $DaysOld = ([DateTime]::Now.Date - [DateTime]$LastModified.Date) if ($DaysOld.Days -gt $Cleanup){ Write-Host $("Deleting the blob file {0}" -f $blob.Name) Remove-AzStorageBlob -Context $ctx -Container $container -Blob $blob.Name } } } } } function Download-pbiReport { param( [Parameter(Mandatory=$False)] [object]$Workspace ="" , #Array of Workspaces [Parameter(Mandatory=$False)] [String]$Report="*", #Name of Dataset. Can by Array [Parameter(Mandatory=$False)] [String]$Path="C:\temp\PowerBI", #Path to Export to. Can be a Temp Path if [parameter(Mandatory=$false)] [string]$StorageAccountName="", #Azure StorageAccountName for Upload [parameter(Mandatory=$false)] [string]$Container="powerbi", #Azure $Container eg "PowerBI" [parameter(Mandatory=$false)] [string]$SAS="" #SAS Token for upload ) $ErrorActionPreference = "Stop" if (-Not $workspace) { [System.Collections.ArrayList]$workspace=Get-PowerBIWorkspace | select-Object Name } if ($SAS) { if ((Get-Command "AzCopy.exe" -ErrorAction SilentlyContinue) -eq $null) { Write-Error "Unable to find AzCopy in your PATH. Please download to C:\Windows or in Path" } } New-Item -ItemType Directory -Force -Path $Path | Out-Null Remove-Item -Path $Path -Force -Recurse -Filter "*.pbix" ForEach ($w in $Workspace) { $WorkspaceId=Get-PowerBIWorkspace -Name $w | Select -ExpandProperty "Id" if (-Not $WorkspaceId) { write-Error "Workspace '$w' Not Found. Check Workspaces and Permissions"} Get-PowerBIReport -WorkspaceId $WorkspaceId | Where {$_.Name -like $Report} | ForEach-Object { $ds=Get-PowerBIDataset -Id $_.DataSetId -WorkspaceId $WorkspaceId if ($ds.IsEffectiveIdentityRequired -eq $True) { $pbix=$_.Name + "_"+ (Get-Date -Format "yyyyMMdd") + ".pbix" $file=Join-Path -Path $Path -ChildPath $w New-Item -ItemType Directory -Force -Path $file | Out-Null $file=Join-Path -Path $file -ChildPath ${pbix} if (Test-Path $file) { Remove-Item $file } "Downloading {0}" -f $file $Id =$_.Id $Url = "groups/$WorkspaceId/reports/$Id/Export" Invoke-PowerBIRestMethod -Url $Url -Method get -OutFile $file #Export-PowerBIReport -Id $_.Id -OutFile $file } } } if ($SAS) { $uri = "https://${StorageAccountName}.blob.core.windows.net/${Container}?$SAS" AzCopy cp $Path $uri --recursive=true } } export-modulemember -function New-pbiReport export-modulemember -function Remove-pbiReport export-modulemember -function Set-pbiRebindReport export-modulemember -function Set-pbiDatasetTakeOver export-modulemember -function Set-pbiDatasetGateway export-modulemember -function Get-pbiStatus export-modulemember -function Invoke-pbiRefresh export-modulemember -function Invoke-pbiRefreshDataSet export-modulemember -function Set-pbiSqlServer export-modulemember -function Import-pbiAuditLogs export-modulemember -function Import-pbiMsolUser export-modulemember -function Import-pbiAuditLogsRestApi export-modulemember -function Download-pbiReport export-modulemember -function Remove-pbiBackupBlob |