Deploy SSRS reports on a customer's machine with PowerShell Deploy SSRS reports on a customer's machine with PowerShell powershell powershell

Deploy SSRS reports on a customer's machine with PowerShell


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  .\ReportingServicesToolsNew-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 = $conWrite-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.FileSystemAdd-Type -AssemblyName System.Collections$unzippedList = New-Object System.Collections.ArrayListClass 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?


Upload of a branding package

With this I finally managed to upload a branding package

This stored procedure will read and write all Catalog entries:

CREATE PROCEDURE InstallBrandingPackage(    --Arguments     @BrandingZIPPath VARCHAR(300)    ,@BrandingColorsPath VARCHAR(300)    ,@BrandingLogopath VARCHAR(300)    ,@BrandingType VARCHAR(100)    ,@BrandingName VARCHAR(100)    ,@BrandingVersion VARCHAR(10))ASBEGIN    BEGIN TRANSACTION;    SET XACT_ABORT ON;    DECLARE @BrandingZIPName VARCHAR(150)=RIGHT(@BrandingZIPPath,CHARINDEX('\',REVERSE(@BrandingZIPPath))-1);    DECLARE @BrandingZIP VARBINARY(MAX);    DECLARE @BrandingColorsJSON VARBINARY(MAX);    DECLARE @BrandingLogoPNG VARBINARY(MAX);    CREATE TABLE #tmpBranding(FileType VARCHAR(100),Content VARBINARY(MAX));    DECLARE @cmd VARCHAR(MAX);    SET @cmd=    'INSERT INTO #tmpBranding(FileType,Content)        SELECT ''zip'',BulkColumn FROM OPENROWSET(BULK ''' + @BrandingZIPPath + ''', SINGLE_BLOB) AS x';    EXEC(@cmd);    SET @cmd=    'INSERT INTO #tmpBranding(FileType,Content)        SELECT ''colors'',BulkColumn FROM OPENROWSET(BULK ''' + @BrandingColorsPath + ''', SINGLE_BLOB) AS x';    EXEC(@cmd);    SET @cmd=    'INSERT INTO #tmpBranding(FileType,Content)        SELECT ''logo'',BulkColumn FROM OPENROWSET(BULK ''' + @BrandingLogopath + ''', SINGLE_BLOB) AS x';    EXEC(@cmd);    SET @BrandingZIP=(SELECT Content FROM #tmpBranding WHERE FileType='zip');    SET @BrandingColorsJSON=(SELECT Content FROM #tmpBranding WHERE FileType='colors');    SET @BrandingLogoPNG=(SELECT Content FROM #tmpBranding WHERE FileType='logo');    --needed variables and IDs taken from System Resources    DECLARE @SystemResourceID UNIQUEIDENTIFIER           ,@SystemResourcePath VARCHAR(500)           ,@PolicyID UNIQUEIDENTIFIER           ,@UserID UNIQUEIDENTIFIER;    SELECT TOP 1 @SystemResourceID=ItemID                ,@SystemResourcePath=[Path]                ,@PolicyID=PolicyID                 ,@UserID=CreatedByID     FROM ReportServer.dbo.[Catalog] WHERE [Name] = 'System Resources';    --Delete all existing    DELETE FROM ReportServer.dbo.[Catalog] WHERE [Path] LIKE '%' +  @BrandingType + '%';    --New Variables    DECLARE @NewZipID UNIQUEIDENTIFIER = NEWID();    DECLARE @NewPathID UNIQUEIDENTIFIER = NEWID();    DECLARE @NewPath VARCHAR(100) = '/' + LOWER(CAST(NEWID() AS VARCHAR(100)));    DECLARE @NewPathName VARCHAR(100) ='fbac82c8-9bad-4dba-929f-c04e7ca4111f'; --It seems, that this special GUID is needed, otherwise no Logo is displayed    DECLARE @NewBrandID UNIQUEIDENTIFIER = NEWID();    DECLARE @ColorsID UNIQUEIDENTIFIER = NEWID();    DECLARE @LogoID UNIQUEIDENTIFIER = NEWID();    DECLARE @dt DATETIME=GETDATE();    DECLARE @BrandProperties NVARCHAR(MAX)=    CAST((        SELECT @BrandingType AS [Resource.Type]              ,@BrandingName AS [Resource.Name]              ,@BrandingVersion AS [Resource.Version]              ,LOWER(CAST(@NewZipID AS VARCHAR(100))) AS [Resource.PackageId]              ,LOWER(CAST(@ColorsID AS VARCHAR(100))) AS [Item.colors]              ,LOWER(CAST(@LogoID AS VARCHAR(100))) AS [Item.logo]        FOR XML PATH('Properties'),TYPE    ) AS NVARCHAR(MAX));    --Universal Brand    INSERT INTO ReportServer.dbo.[Catalog](ItemID,[Path],[Name],ParentID,[Type]               ,Content               ,Property               ,[Hidden],CreatedByID,CreationDate,ModifiedByID,ModifiedDate               ,MimeType,PolicyID,PolicyRoot,ExecutionFlag)      VALUES(@NewBrandID,@SystemResourcePath + '/' + @BrandingType, @BrandingType, @SystemResourceID,1          ,NULL          ,@BrandProperties          ,0,@UserID,@dt,@UserID,@dt,NULL,@PolicyID,0,1);    --ZIP file dummy    DECLARE @currentPath VARCHAR(500) = @SystemResourcePath + '/' +  @BrandingType + '/' + @BrandingZIPName;    INSERT INTO ReportServer.dbo.[Catalog](ItemID,[Path],[Name],ParentID,[Type]               ,Content               ,Property               ,[Hidden],CreatedByID,CreationDate,ModifiedByID,ModifiedDate               ,MimeType,PolicyID,PolicyRoot,ExecutionFlag)      VALUES(@NewZipID,@currentPath,@BrandingZIPName,@NewBrandID,3          ,@BrandingZIP          ,'<Properties />'          ,0,@UserID,@dt,@UserID,@dt,'application/octet-stream',@PolicyID,0,1);    --Brand path    SET @currentPath = @SystemResourcePath + '/' +  @BrandingType + '/' + @NewPathName;    INSERT INTO ReportServer.dbo.[Catalog](ItemID,[Path],[Name],ParentID,[Type]               ,Content               ,Property               ,[Hidden],CreatedByID,CreationDate,ModifiedByID,ModifiedDate               ,MimeType,PolicyID,PolicyRoot,ExecutionFlag)      VALUES(@NewPathID,@currentPath,@NewPathName,@NewBrandID,1          ,NULL          ,'<Properties />'          ,0,@UserID,@dt,@UserID,@dt,NULL,@PolicyID,0,1);    --colors    INSERT INTO ReportServer.dbo.[Catalog](ItemID,[Path],[Name],ParentID,[Type]               ,Content               ,Property               ,[Hidden],CreatedByID,CreationDate,ModifiedByID,ModifiedDate               ,MimeType,PolicyID,PolicyRoot,ExecutionFlag)      VALUES(@ColorsID,@currentPath + '/colors','colors',@NewPathID,3          ,@BrandingColorsJSON          ,'<Properties />'          ,0,@UserID,@dt,@UserID,@dt,'application/octet-stream',@PolicyID,0,1);    --logo    INSERT INTO ReportServer.dbo.[Catalog](ItemID,[Path],[Name],ParentID,[Type]               ,Content               ,Property               ,[Hidden],CreatedByID,CreationDate,ModifiedByID,ModifiedDate               ,MimeType,PolicyID,PolicyRoot,ExecutionFlag)      VALUES(@LogoID,@currentPath + '/logo','logo',@NewPathID,3          ,@BrandingLogoPNG          ,'<Properties />'          ,0,@UserID,@dt,@UserID,@dt,'image/png',@PolicyID,0,1);    COMMIT;END

And with this PowerShell lines I unzip the package, extract the parameters and call the procedure:

#Unzip BrandingPackage Add-Type -AssemblyName System.IO.Compression.FileSystemAdd-Type -AssemblyName System.Collections$BrandingPackagePath = ($PSScriptRoot + "\FrequentisReportCenter.zip")$BrandingPackageUnzipped = ($PSScriptRoot + "\FrequentisReportCenter.zip.Unzipped")[System.IO.Compression.ZipFile]::ExtractToDirectory($BrandingPackagePath,$BrandingPackageUnzipped)Write-Host "BrandingPackage unzipped"$xml = New-Object System.Xml.XmlDocument$nsMngr = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)$nsMngr.AddNamespace("ns","http://schemas.microsoft.com/sqlserver/reporting/2016/01/systemresourcepackagemetadata")$xml.Load(($BrandingPackageUnzipped + "\" + "metadata.xml"))$BrandingType=$xml["SystemResourcePackage"].Attributes["type"].Value$BrandingVersion=$xml["SystemResourcePackage"].Attributes["version"].Value$BrandingName=$xml["SystemResourcePackage"].Attributes["name"].Value$PathColors=($BrandingPackageUnzipped + "\" + $xml.SelectNodes("//ns:Contents/ns:Item[@key='colors']",$nsMngr)[0].Attributes["path"].Value)$PathLogo=($BrandingPackageUnzipped + "\" + $xml.SelectNodes("//ns:Contents/ns:Item[@key='logo']",$nsMngr)[0].Attributes["path"].Value)#BrandingPackage values fetched$cmd.CommandText="MyDatabase.dbo.InstallBrandingPackage `                                                  @BrandingZIPPath=@ZIPPath,`                                                  @BrandingColorsPath=@ColorsPath,`                                                  @BrandingLogoPath=@LogoPath,`                                                  @BrandingType=@Type,`                                                  @BrandingName=@Name,`                                                  @BrandingVersion=@Version"$cmd.Parameters.AddWithValue("@ZIPPath",$BrandingPackagePath)$cmd.Parameters.AddWithValue("@ColorsPath",$PathColors)$cmd.Parameters.AddWithValue("@LogoPath",$PathLogo)$cmd.Parameters.AddWithValue("@Type",$BrandingType)$cmd.Parameters.AddWithValue("@Name",$BrandingName)$cmd.Parameters.AddWithValue("@Version",$BrandingVersion)$cmd.ExecuteNonQuery()#BrandingPackage written