Recently we started using the SQL Data Tools preview in 2015, its looking good so far, for the first time in a long time I don’t have to “match” the visual studio version I’m using with SQL, i can use the latest version of VS and support SQL version from 2008 to 2016, which is great, but it isn’t perfect. There is a lot of bugs in the latest version of data tools and some that Microsoft are refusing to fix that affect us.
One of the sticking points for me has always been deployment, we use octopus internally for deployment and try to run everything through that if we can to simplify our deployment environment.
SSRS is the big one, so I’m going to go into how we do deployments of that.
We use a tools in SQL called RS.EXE this is a command line tool that pre-installs with SSRS for running vb scripts to deploy reports and other objects in SSRS.
You need to be aware with this tool though that based on which API endpoint you call using the “-e” command line parameter, that the methods will be different. And out of the box it defaults to calling the 2005 endpoint, which has massive changes to 2010.
A lot of the code i wrote was based on Tom’s Blog Post on the subject in 2011, however he was using the 2005 endpoint and I have updated the code to use the 2010 end point.
The assumption is that you will have a folder full of RDL files (and possibly some pngs and connection objects) that you need to deploy, so the VB script takes a parameters of a “source folder”, the “target folder” on the SSRS server, and the “SSRS server address” itself. I package this into a predeploy.ps1 file that I package up into a nuget file for octopus, the line in the pre-deploy script looks like the below.
rs.exe -i Deploy_VBScript.rss -s $SSRS_Server_Address -e Mgmt2010 -v sourcePATH="$sourcePath" -v targetFolder="$targetFolder"
You will note the “-e Mgmt2010” this is important for the script to work, next is the Deploy_VBScript.rss
Dim definition As [Byte]() = Nothing Dim warnings As Warning() = Nothing Public Sub Main() ' Create Folder for the project if not exist try Dim descriptionProp As New [Property] descriptionProp.Name = "Description" descriptionProp.Value = "" Dim visibleProp As New [Property] visibleProp.Name = "Visible" visibleProp.value = True Dim props(1) As [Property] props(0) = descriptionProp props(1) = visibleProp If targetFolder.SubString(1).IndexOf("/") <> -1 Then Dim level2 As String = targetFolder.SubString(targetFolder.LastIndexOf("/") + 1) Dim level1 As String = targetFolder.SubString(1, targetFolder.LastIndexOf("/") - 1) Console.WriteLine(level1) Console.Writeline(level2) rs.CreateFolder(level1,"/", props) rs.CreateFolder(level2 , "/"+level1, props) Else Console.Writeline(targetFolder.Replace("/", "")) rs.CreateFolder(targetFolder.Replace("/", ""), "/", props) End If Catch ex As Exception If ex.Message.Indexof("AlreadyExists") > 0 Then Console.WriteLine("Folder {0} exists on the server",targetFolder) else throw ex End If End Try Dim Files As String() Dim rdlFile As String Files = Directory.GetFiles(sourcePath) For Each rdlFile In Files If rdlFile.EndsWith(".rds") Then 'TODO Implment handler for RDS files End If If rdlFile.EndsWith(".rsd") Then 'TODO Implment handler for RSD files End If If rdlFile.EndsWith(".png") Then Console.WriteLine(String.Format("Deploying PNG file {2} {0} to folder {1}", rdlFile, targetFolder, sourcePath)) Dim stream As FileStream = File.OpenRead(rdlFile) Dim x As Integer = rdlFile.LastIndexOf("\") definition = New [Byte](stream.Length) {} stream.Read(definition, 0, CInt(stream.Length)) Dim fileName As String = rdlFile.Substring(x + 1) Dim prop As new [Property] prop.Name ="MimeType" prop.Value="image/png" Dim props(0) as [Property] props(0)=prop rs.CreateCatalogItem("Resource",fileName,targetFolder,True,definition,props,Nothing) End If If rdlFile.EndsWith(".rdl") Then Console.WriteLine(String.Format("Deploying report {2} {0} to folder {1}", rdlFile, targetFolder, sourcePath)) Try Dim stream As FileStream = File.OpenRead(rdlFile) Dim x As Integer = rdlFile.LastIndexOf("\") Dim reportName As String = rdlFile.Substring(x + 1).Replace(".rdl", "") definition = New [Byte](stream.Length - 1) {} stream.Read(definition, 0, CInt(stream.Length)) rs.CreateCatalogItem("Report",reportName, targetFolder, True, definition, Nothing, warnings) If Not (warnings Is Nothing) Then Dim warning As Warning For Each warning In warnings Console.WriteLine(warning.Message) Next warning Else Console.WriteLine("Report: {0} published successfully with no warnings", reportName) End If Catch e As Exception Console.WriteLine(e.Message) End Try End If Next End Sub
I haven’t yet added support for RDS and RSD files to this script. If someone wants to finish it off please share. Now its running PNGs and RDLs which is the main thing we update.
Now that all sounds too easy, and it was, the next issue i ran into was this one when deploying. The VS 2015 Data Tools client saves everything in 2016 format, and when I say 2016 format, i mean it changes the xmlns and adds a tag called “ReportParametersLayout”.
When you deploy from the VS client it appears to “rollback” these changes before passing the report to the 2010 endpoint, but if you try to deploy the RDL file from source it will fail (insert fun).
To work around this I had to write my own “roll back” script in the octopus pre-deploy powershell script, below:
Get-ChildItem $sourcePath -Filter "*.rdl" | ` Foreach-Object{ [Xml]$xml = [xml](Get-Content $_.FullName) if($xml.Report.GetAttribute("xmlns") -eq "http://schemas.microsoft.com/sqlserver/reporting/2016/01/reportdefinition") { $xml.Report.SetAttribute("xmlns","http://schemas.microsoft.com/sqlserver/reporting/2010/01/reportdefinition") } if($xml.Report.ReportParametersLayout -ne $null) { $xml.Report.RemoveChild($xml.Report.ReportParametersLayout) } $xml.Save($sourcePath + '\' + $_.BaseName + '.rdl') }
Now things were deploying w… no, wait! there’s more!
Another error I hit was this next one, I mentioned before MS are refusing to fix some bugs here’s one. We work in Australia, so Time Zones are a pain in the ass, we have some states on daylight savings, some not, and some that were that have now stopped. Add to this we work on a multi-tenanted application, so while some companies rule “We are in Sydney time, end of story” we cannot. So using the .NET functions for timezone conversion is a must as they take care of all this nonsense for you.
I make a habit of storing all date/time data in UTC that way in the database or application layer if you need to do a comparison its easy, then i convert to local timezone in the UI based on user preference or business rule, etc. Its because of this we have been using System.TimezoneInfo in the RDL files. As of VS 2012 and up, you get an error from this library in VS, the report will still run fine on SSRS when deployed, it just errors in the Editor in VS and wont preview or deploy.
We’ve worked around this by using a CLR function that wraps TimezonInfo example here. If you are reading this please vote this one up on connect, converting timezone in the UI is better.
After this fix things are running smoothly.