Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Deploy SSRS reports on a customer's machine with PowerShell

We are developing some reports locally. These reports are to be installed on a customer's machine. But we cannot reach this machine online.

What I've managed so far:

A folder containing:

  • all report objects (data source, data sets and reports)
  • a copy of the PowerShell module ReportingServicesTools
  • a PowerShell script

This script will create all needed folders within ReportServer, load the data source, then the data sets and finally the reports. This works, I can open the portal and can see my objects.

UPDATE:

The downloaded rdl files had their data-set-references set to the plain dataset's name - why ever... It took me hours to find, that the reference must include the full (internal) path to the dataset... Now the paginated reports really work, heureka!

The Questions

This does not work for

  • mobile reports (aka dashboards, but not PowerBI)
  • our branding file

How can mobile reports be uploaded via PowerShell?
How can a custom branding package be uploaded?
Is there a better approach? I'm fairly new to PowerShell and therefore open to any advice! TIA

EDIT

Deleted the script fragment as I've posted a comprehensive answer myself...

UPDATE: No answer yet?

At the bottom of my own answer there are some open questions... I'd love to give the bounty to somebody but it seems to be lost... Anyway: If Anybode might answer at least part of the remaining open questions there's a good chance for 150 points :-)

like image 867
Shnugo Avatar asked Nov 30 '17 18:11

Shnugo


People also ask

How do I deploy an SSRS report in Visual Studio?

To publish all reports in a projectOn the Build menu, click Deploy <report project name>. Alternatively, in Solution Explorer, right-click the report project and then click Deploy.

How deploy RDL report on server?

By Uploading RDL file in Report Server.Open SSRS Server from webportal URL. There, you will see the upload button. Click the upload option and browse the rdl file of the report from the location. It uploads your report to the report server.


1 Answers

Microsoft decided to let us poor tech staff alone with this...

The issues I've found so far:

  • After deletion of shared dataset all mobile reports must be re-created from scratch (a re-mapping of datasources is not supported). This is by design, read this, if you cannot believe it
  • Deployment is supported to a reachable server only (via target URL)
  • There is currently no support to upload a mobile report other than via SSRS portal
  • There is currently no support to upload a branding package other than via SSRS portal

As nobody seems to be able to help - even with a bounty! - I'll state what I've found so far:

The following PowerShellScript is a working stand-alone deployment on a disconnected machine.

Assumptions:

  • All SSRS-objects (.rsd, .rdl and .rsmobile) are downloaded and are living in appropriate directories below "DeployDirectory"
    • In my case all shared data set filles are in folder "MyProduct Data"
    • In my case all reports are living within sub-folders "EventLog", "Public Reports" and "Report Audit"
    • It would be possible to automate the folders structure, but in this script this part is hard-coded to my needs.
  • The PowerShell-Module ReportingServicesTools is placed within a directory with the same name below "DeployDirectory"
  • A shared data set's name is globally unique (this is not mandatory by SSRS)

Good luck with this!

$DeployDirectory=".\MyProduct Reports\"
$DbURL="1.2.3.4"
$srvURI="http://1.2.3.4/ReportServer"
$srvURL="http://1.2.3.4/reports"

#Write RS Report folders
$rootDir="/MyProduct Reports"

#Import the module and create folders. This might be automated too (out of the directory structure found on disc, but this is up to you...)

Import-Module  .\ReportingServicesTools
New-RsFolder -ReportServerUri $srvURI -Path "/"      -Name "MyProduct Reports"
New-RsFolder -ReportServerUri $srvURI -Path $rootDir -Name "MyProduct Data" 
New-RsFolder -ReportServerUri $srvURI -Path $rootDir -Name "EventLog" 
New-RsFolder -ReportServerUri $srvURI -Path $rootDir -Name "Public Reports" 
New-RsFolder -ReportServerUri $srvURI -Path $rootDir -Name "Report Audit" 
Write-Host "RS Report folders written"

#Create shared data source

New-RsDataSource -RsFolder ($rootDir + "/MyProduct Data") -Name "MyProduct_DatabaseConnection" -Extension "SQL" -ConnectionString ("Data Source=" +  $DbURL + ";Integrated Security=True;Initial Catalog=master;") -CredentialRetrieval "Integrated"
Write-Host "Shared data source created"

#Modify shared data set files: The hardcoded reference to the server's URL must be changed

$allRSDs = Get-ChildItem -Recurse -Path $DeployDirectory | Where-Object -FilterScript {$_.Extension -eq ".rsd"}
Write-Host "RSDs fetched"

$xml = New-Object System.Xml.XmlDocument
$nsMngr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$nsMngr.AddNamespace("rd","http://schemas.microsoft.com/SQLServer/reporting/reportdesigner")
$allRSDs | % {
               $FileName=$_.FullName;
               $xml.Load($FileName);
               $xml.SelectNodes("//rd:ReportServerUrl",$nsMngr) | 
               % {$_.InnerText=$srvURI};
               $newContent = $xml.InnerXml;
               Set-Content -Path $FileName -Value $newContent
             }
Write-Host "Shared data set files modified"

Write-RsFolderContent -Recurse -ReportServerUri $srvURI -Path ($DeployDirectory + "MyProduct Data") -RsFolder ($rootDir + "/MyProduct Data") 
Write-Host "Shared DataSets created"

#Read all created shared data sets out of the database into a table variable

$con = New-Object System.Data.SqlClient.SqlConnection
$con.ConnectionString=("Data Source=" + $DbURL + ";Integrated Security=True;Initial Catalog=master;")
$con.Open();
Write-Host "connection opened"

$cmd = New-Object System.Data.SqlClient.SqlCommand
$cmd.Connection = $con
Write-Host "command created"

$cmd.CommandText = "SELECT ItemID, [name] AS DataSetName, [Path] AS DataSetPath, LEN([name]) AS SortOrder `
                    FROM ReportServer.dbo.[Catalog]` 
                    WHERE [Type]=8"
$adapt = New-Object System.Data.SqlClient.SqlDataAdapter
$adapt.SelectCommand = $cmd
$ds = New-Object System.Data.DataSet
$adapt.Fill($ds)
$allDs = New-Object System.Data.DataTable
$allDs = $ds.Tables[0]
Write-Host "shared datasets fetched into cache"

Class shDs {[string]$ItemId=""; [string]$DataSetName=""; [string]$DataSetPath="";}
function Get-SharedDataSet([string]$DataSetName){
    $retVal = New-Object shDs
    $Search = ("'" + $DataSetName + "' LIKE DataSetName + '%'") 
    $Sort = ("SortOrder DESC")
    $dsRow = $allDs.Select($Search,$Sort)[0]
    $retVal.ItemID=$dsRow["ItemID"].ToString().Trim()
    $retVal.DataSetPath=$dsRow["DataSetPath"].ToString().Trim()
    $retVal.DataSetName=$dsRow["DataSetName"].ToString().Trim()
    return $retVal
}
Write-Host "function to fetch referenced shared dataset created"
$con.Close()
Write-Host "connection closed"

#Modify paginated report files: The newly written shared datasets must be written into the report-XML

$allRDLs = (Get-ChildItem -Recurse -Path $DeployDirectory | 
                Where-Object -FilterScript {$_.Extension -eq ".rdl"})
$xml = New-Object System.Xml.XmlDocument
$nsMngr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$nsMngr.AddNamespace("rd","http://schemas.microsoft.com/SQLServer/reporting/reportdesigner")
$nsMngr.AddNamespace("ns","http://schemas.microsoft.com/sqlserver/reporting/2016/01/reportdefinition")
$allRDLs | % {
               $FileName=$_.FullName;
               $xml.Load($FileName);
               $xml.SelectNodes("//rd:ReportServerUrl",$nsMngr) | 
                  % {$_.InnerText=$srvURI};
               $xml.SelectNodes("//ns:SharedDataSetReference",$nsMngr) | 
                  % {
                       $it = ("/" + $_.Innertext);
                       $ref=$it.SubString($it.LastIndexOf("/")+1);
                       $ds = Get-SharedDataSet($ref);
                       $_.InnerText=$ds.DataSetPath
                       Write-Host ("DataSetPath: " + $_.InnerText)
                    };
               $newContent = $xml.InnerXml;
               Set-Content -Path $FileName -Value $newContent
             }
Write-Host "paginated report files modified"

Write-RsFolderContent -Recurse -ReportServerUri $srvURI -Path ($DeployDirectory + "EventLog") -RsFolder ($rootDir + "/EventLog") 
Write-RsFolderContent -Recurse -ReportServerUri $srvURI -Path ($DeployDirectory + "Public Reports") -RsFolder ($rootDir + "/Public Reports") 
Write-RsFolderContent -Recurse -ReportServerUri $srvURI -Path ($DeployDirectory + "Report Audit") -RsFolder ($rootDir + "/Report Audit")
Write-Host "paginated reports created"

#Modify mobile report files: This is more complicated... The files are ZIPs actually. These ZIPs contain several files. The metadata.xml and the sources.xml contain hard-coded references and must be changed

$allMobs = (Get-ChildItem -Recurse -Path $DeployDirectory | 
                Where-Object -FilterScript {$_.Extension -eq ".rsmobile"})

#Unzip SomeName.rsmobile into SomeName.rsmobile.Unzipped 
Add-Type -AssemblyName System.IO.Compression.FileSystem
Add-Type -AssemblyName System.Collections
$unzippedList = New-Object System.Collections.ArrayList
Class zippedMobs {[string]$DirectoryName; [string]$rsMobileName; [string]$FileName;}
Get-ChildItem -Recurse $path | 
    Where-Object -FilterScript {$_.Extension -eq ".rsmobile"} | 
    % {
       $zm = New-Object zippedMobs;
       $zm.DirectoryName = ($_.FullName + ".Unzipped");
       $zm.rsMobileName=$_.FullName;
       $zm.FileName=$_.Name;
       $unzippedList.Add($zm);
       [System.IO.Compression.ZipFile]::ExtractToDirectory($zm.rsMobileName,$zm.DirectoryName)
      }
Write-Host "Mobile Reports: Files unzipped"

#Open all metadata.xml files in all unzipped folders and modify them

$xml = New-Object System.Xml.XmlDocument
$nsMngr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$nsMngr.AddNamespace("ns","http://schemas.microsoft.com/sqlserver/reporting/2016/02/mobilereportpackage")
$nsMngr.AddNamespace("mrp","http://schemas.microsoft.com/sqlserver/reporting/2016/02/mobilereportpublisher")
$unzippedList | % {
         $FileName=($_.DirectoryName + "\metadata.xml");
         $xml.Load($FileName);
         $xml.SelectNodes("//ns:dataSet",$nsMngr) | 
            % {
                $ref=$_.Attributes["name"].Value;
                $ds = Get-SharedDataSet($ref);
                $_.Attributes["mrp:Server"].Value=$srvURL;
                $_["id"].InnerText=$ds.ItemID;
                $_["path"].InnerText=$ds.DataSetPath
              };
         $newContent = $xml.InnerXml;
         Set-Content -Path $FileName -Value $newContent
        }
Write-Host "Mobile Reports: All metadata.xml re-mapped"

#Open all sources.xml files in all unzipped folders and modify them

$xml = New-Object System.Xml.XmlDocument
$unzippedList | % {
         $FileName=($_.DirectoryName + "\sources.xml");
         $xml.Load($FileName);
         $xml.SelectNodes("//Shared") | 
            % { 
             $ref=$_.Attributes["Name"].Value;
             $ds = Get-SharedDataSet($ref);
             $_.Attributes["ServerUri"].Value=$srvURL; 
             $_.Attributes["Guid"].Value=$ds.ItemID;
             $_.Attributes["DataItemLocation"].Value=$ds.DataSetPath
            };
         $newContent = $xml.InnerXml; 
         Set-Content -Path $FileName -Value $newContent
        }
Write-Host "Mobile Reports: All sources.xml re-mapped"

#Rename all original .rsmobile files

$unzippedList | % {Rename-Item -Path $_.rsMobileName -NewName ($_.FileName + ".old")}
Write-Host "Mobile Reports: Renamed all orginal .rsmobile files"
#Create new ZIP file for all mobile reports
$unzippedList | % {
                   [System.IO.Compression.ZipFile]::CreateFromDirectory($_.DirectoryName,$_.rsMobileName,[System.IO.Compression.CompressionLevel]::NoCompression, $FALSE)
                  }
Write-Host "Re-created all mobile report files"

Attention

Allthough the created ZIP-files (the new .rsmobile files) are valid and contain the correct content, it is impossible to upload them via SSRS portal (error: invalid report package). But - funny enough! - when you use explorer's send to compressed directory and rename the resulting ZIP file accordingly, this can be uploaded.

Still open questions:

  • How can one create the .rsmobile (ZIP-file) that it is uploadable? (must be the same way of zipping as windows does it implicitly when sending to a compressed folder)
  • How can one upload a mobile report programmatically?

This one I could answer in the meanwhile (see second answer):

  • How can one upload a branding package programmatically?
like image 110
Shnugo Avatar answered Oct 19 '22 09:10

Shnugo