Wednesday, June 15, 2016

Powershell Script - New Version of my SSRS Bulk Upload

This Powershell script takes all report files(.rdl) in a folder ,creates the folder structure on the server ,uploads the reports and changes the data sources.

If you liked the first version of this script ,you'll love the new version.
The first version was written to solve a problem and nothing more, but turned out that it was something the  SSRS community was waiting for as it is one of the most read posts on my blog.

The new version has been completely rewritten to make it more robust and in the process i've added a feature or two.
  • It now uses the Reporting Service 2010 Web Service
  • It's now structurally a better script. Easier to understand and work with.
  • Supports Folder structure
  • Can easily be modified to upload all kinds of elements like images,pdf+
The script is designed for Configmgr reports ,but the only part that's specific to Configmgr is the part that searches for the data source. To work with other data sources you just need to modify the search criteria or just "hard code" in the name of the shared data source you want to use.

How It works

The new script has default values on some of the parameters to make it easier to use.
only required parameter is the URL(-webserviceUrl) to the reporting server.

& '.\Upload Reports v3.ps1' -webServiceUrl http://yourSSRSserver

Default it will look in the script folder for reports ,upload them to a folder called "Larsen Reports" and wont overwrite if report already exist ,but all this can be changed by setting additional parameters.

  & '.\Upload Reports v3.ps1' -webServiceUrl http://yourSSRSserver -SourceDirectory d\Reports  -ReportFolder Demo -overwrite $true


In this example i just threw a few reports in a folder/sub folder structure. Some reports on the root and some in the sub directories.


I run the script assigning the -reportFolder paramenter to Demo.

As you can see in the screenshot under it first creates the folder structure, then it searches for *.rdl files and upload them to the correct folder/ sub folder and changes the data source.

This powershell script takes all report files(.rdl) ,creates the folder structure on the server ,uploads the reports and changes the datasources.

Everything should be ready to use!

Looking at the Report server, it has created the folders Demo and the 3 sub folders. The reports are uploaded in the right place.



..and the data source has been changed to the correct one.



Download: Upload Reports v3.zip

<#
.SYNOPSIS
    Bulk import SSRS Reports from a folder and update the reports data source.
.DESCRIPTION
    This script takes all the RDL files in a folder and imports them into a SSRS and updates the reports data source to the Configmgr shared data source.
.NOTES
    File Name  : Upload Reports.ps1
    Version    : 3.00 ,13.jun-2016
    Author     : Thomas Larsen - thomas.larsen@tromsfylke.no
    Requires   : Version 3
.LINK
    Blog
      http://larsenconfigmgr.blogspot.com

.PARAMETER webServiceUrl
Base URL to the report server ,usually the Configmgr Site server

.PARAMETER reportFolder
 Report Server folder where the reports are imported to, the script creates the folder if it's not there.
 
.PARAMETER SourceDirectory
The local folder where the .rdl files are located.

.PARAMETER Overwrite
Set to True if Script should overwrite Reports it they already exist.

.Example

& '.\Upload Reports.ps1' -webServiceUrl "http://Reportserver.domain.local"
Takes the report files from current directory(And Subdirectories) and uploads them the to a folder on SSRS called Larsen Reports(Default)

.Example

& '.\Upload Reports.ps1' -webServiceUrl "http://Reportserver.domain.local" -reportFolder "Larsen Reports -SourceDirectory "c:\ReportsToUpload" -overwrite $True
All Parameters ,select Sourcefolder Destination Folder ,Overwrite if exist


#>

Param(
   [string]$webServiceUrl,
      
   [string]$reportFolder = "Larsen Reports",

   [string]$SourceDirectory = $PSScriptRoot,

   [bool]$overwrite = $false
   )

Write-Host "Thomas Larsen - December 2015 - http://larsenconfigmgr.blogspot.com" -ForegroundColor Cyan
Write-Host "This Script is provided AS-IS, no warrenty is provided" 
Write-host ""

#Connect to SSRS
Write-Host "Reportserver: $webServiceUrl" -ForegroundColor DarkMagenta
Write-Host "Estabishing Proxy connection, connecting to : $webServiceUrl/ReportServer/ReportService2010.asmx?WSDL"
Write-Host ""

$ssrsProxy = New-WebServiceProxy -Uri $webServiceUrl'/ReportServer/ReportService2010.asmx?WSDL' -UseDefaultCredential

#######
#Get Configmgr shared datasource
$Items = $ssrsProxy.listchildren("/", $true) | where {$_.TypeName -eq "Datasource" }
foreach ($item in $items)
    {
    #Check to see if Datasource name patches Guid Pattern
    if ($item.name -match '{([a-zA-Z0-9]{8})-([a-zA-Z0-9]{4})-([a-zA-Z0-9]{4})-([a-zA-Z0-9]{4})-([a-zA-Z0-9]{12})}' -and $item.path -like '/ConfigMgr*')
        {
        Write-Host "Datasource:" $item.Name -ForegroundColor Magenta  
        Write-host "Type:" $item.TypeName 
        Write-Host "Path:" $item.Path

        #Save parameters for later use.
        $DatasourceName = $item.Name
        $DatasourcePath = $item.Path
        }
    }

function SSRSFolder ([string]$reportFolder,[string]$reportPath)
{
##########################################    
#Create Folder     
        try
        {
            $ssrsProxy.CreateFolder($reportFolder, $reportPath, $null) | out-null
            if($reportPath -eq '/')
            {
            Write-Host "Folder `"$reportpath$reportFolder`" Created"
            }
            else
            {
            Write-Host "Folder `"$reportpath/$reportFolder`" Created"
            }
        }
        catch [System.Web.Services.Protocols.SoapException]
        {
            if ($_.Exception.Detail.InnerText -match "rsItemAlreadyExists400")
            {
                Write-Host "Folder: $reportFolder already exists."
            }
            else
            {
                $msg = "Error creating folder: $reportFolder. Msg: '{0}'" -f $_.Exception.Detail.InnerText
                Write-Error $msg
            }
        }
}

Function SSRSItem ([string]$ItemType,$item,[string]$folder)
 {
 Write-host ""
 #ReportName
 if ($ItemType -ne "Resource")
 {
 $ItemName = [System.IO.Path]::GetFileNameWithoutExtension($item);
 }
 else
 {
 $ItemName = $item.Name
 }
 write-host $ItemName -ForegroundColor Green 

 #Upload File
     try
    {
        #Get Report content in bytes
        Write-Host "Getting file content of : $item"
        $byteArray = Get-Content $item.FullName -encoding byte
        $msg = "Size: {0} KB" -f "{0:N0}" -f ($byteArray.Length/1KB) 
        Write-Host $msg 
        
        Write-Host "Uploading to: $folder"
 
        #Sets property for images(only png)
        $type = $ssrsProxy.GetType().Namespace
        $datatype = ($type + '.Property')              
        $property =New-Object ($datatype);
        if ($ItemType -eq "Resource" -and $item -like "*.png")
        {
        $property.Name = "MimeType"
        $property.Value = image/png
        }
        else
        {
        $property = $null
        }

        #Call Proxy to upload report  
        $warnings =@();    
        $ssrsProxy.CreateCatalogItem($ItemType,$itemName,$folder,$overwrite,$byteArray,$property,[ref]$warnings) | out-null
        if($warnings.Length -le 1) { Write-Host "Upload Success." -ForegroundColor Green }
        else 
        {        
            foreach ($message in $warnings)
                {
                if ($message.Code -ne "rsDataSourceReferenceNotPublished")
                    {
                    write-host "$($message.Severity) $($message.Code) $($message.Message)" -ForegroundColor Yellow
                    }
                }
        }
    }
    catch [System.IO.IOException]
    {
        $msg = "Error while reading rdl file : '{0}', Message: '{1}'" -f $rdlFile, $_.Exception.Message
        Write-Error msg
    }
    catch [System.Web.Services.Protocols.SoapException]
 {
    $caught = $false
            if ($_.Exception.Detail.InnerText -match 'rsItemAlreadyExists400')
            {
                $caught = $true
                Write-Host "Report: $itemName already exists." -ForegroundColor Red
            }
            if ($_.Exception.Detail.InnerText -match 'CoretechSSRS')
            {
                $caught = $true
                Write-Host "Cant't find Reporting Extention File." -ForegroundColor Red
            }
            elseif ($caught -eq $false)
            {
                $msg = "Error uploading report: $reportName. Msg: '{0}'" -f $_.Exception.Detail.InnerText
                Write-Error $msg
            }
  
 }
}

Function SSRSDatasource ([string]$ReportPath,$DataSourceName,$DataSourcePath)
{
$report = $ssrsProxy.GetItemDataSources($ReportPath)
ForEach ($Source in $report)
    {
    $proxyNamespace = $Source.GetType().Namespace
        $constDatasource = New-Object ("$proxyNamespace.DataSource")
        $constDatasource.Name = $DataSourceName
        $constDatasource.Item = New-Object ("$proxyNamespace.DataSourceReference")
        $constDatasource.Item.Reference = $DataSourcePath

   $Source.item = $constDatasource.Item
    $ssrsProxy.SetItemDataSources($ReportPath, $Source)
    Write-Host "Changing datasource `"$($Source.Name)`" to $($Source.Item.Reference)"
    }
}

##Create Folder Structure
#Create Base Folder

Write-Host "Creating Folders Structure:" -ForegroundColor DarkMagenta
SSRSFolder $reportFolder "/"

ForEach($Folder in Get-ChildItem $SourceDirectory -Directory)
    {
    #Add each folder in the sourcefolder to the reporting service 
    SSRSFolder $Folder.Name /$reportFolder
    }

     
#Upload Reports in root folder
ForEach ($rdlfile in Get-ChildItem $SourceDirectory -Filter *.rdl )
{
SSRSItem Report $rdlfile /$reportFolder

#Change Report Datasource
$ReportPath = "/" + $reportFolder + "/" +  $rdlfile.BaseName 
SSRSDatasource $ReportPath $DataSourceName $DataSourcePath
}

##For Each folder
 
ForEach($Folder in Get-ChildItem $SourceDirectory -Directory)
    {
    #Add each folder in the sourcefolder to the reporting service 
    #SSRSFolder $Folder.Name /$Basefolder 
    
    #Add reports in the folder
    ForEach ($rdlfile in Get-ChildItem $Folder -Filter *.rdl )
        {
        SSRSItem Report $rdlfile /$reportFolder/$($folder.Name) 

        #Change Report Datasource
        $ReportPath = "/" + $reportFolder + "/" + $folder.Name + "/" + $rdlfile.BaseName 
        SSRSDatasource $ReportPath $DataSourceName $DataSourcePath
        }
    }

Tuesday, June 7, 2016

SSRS Report Speed Problem. How to increase the speed of your SSRS Reports.

Backstory

I tried to show my colleague one of my new reports, but when he tried to open the report it wouldn't load. He could enter the parameters just fine, but the report itself just hung at the "Loading" screen.
Tried the same on my computer and the same thing happened. When i ran the report on the server itself either from the browser or from Report Builder everything worked fine.
My first thought was that this was a speed issue so i started investigating. The problem in this case turned out to be a IE11 problem (setting the report site to comparability mode fixed that), but while I was investigating the issue I found something else that was very interesting.

The Problem

The issue is something called SQL Parameter sniffing and you can read more about it here, Essentially the problem is with the way SQL tries create a execution plan based on the size of the result, but when the query uses parameters the result may vary in size and the execution plan may be far from optimal.
There are several ways to get around this, the easiest way seems to be to add "optimize for unknown" to the query but that requires SQL 2008 R2 or above and may cause a problem for some. But i found a simple workaround while browsing stack exchange that seems to work just fine.

The Solution

The trick is to double up the parameters. Instead of using the @SSRS parameters directly you map the SSRS parameters to SQL Parameters and use the SQL parameters in the query.
I couldn't find a way to do this with multi-value parameters, but it looks like you only need to do this to some of the parameters to bypass the problem.

In the SQL snippit under i take the two SSRS parameters (@Timeperiod and @DefaultTimezone) and create two SQL parameters for them (@TimeperiodVar and @DefaultTimezoneVar) and tie them together. Then I replace the ssrs parameters with the new SQL parameters everywhere they're used in the query. 
DECLARE @TimeperiodVar DATETIME DECLARE @DefaultTimezoneVar INT
--set @AdvertisementIDVar = @AdvertisementID  SET @TimeperiodVar = @Timeperiod SET @DefaultTimezoneVar = @DefaultTimezone; 

The Result

Have a look at the Execution speed on the screenshot under(The Total time is in seconds and the rest is in milliseconds.).
The "OSD Dashboard" is the standard one and "OSD Dashboard Demo" has the new parameter system:



As you can see, I managed to cut the execution time for this report a lot!!
I managed to cut the SQL query time from between 22 seconds down to 0-5 seconds(yesterday it was 47+- seconds for the first one).
SSRS does some caching and so the results may vary, but around one second is what it takes when i run the query in SQL Management Studio.

So this little issue with IE11 Compability mode lead me down a path that enabled me to increase execution speed roughly 27x!

Have tested a few others reports and it seems very promising , I will defently use this on all my reports from now on.








Wednesday, June 1, 2016

OSD Details ,Part 2 of my OSD Dashboard Report

As promised, here is part 2 for my OS Deployment Dashboard.

It's designed to be a drill trough report from my OSD Dashboard,but you can also run it stand alone.
The query has been  completely rewritten from my last report so it should be a lot more reliable than the last version.


To upload the report ,download the zipfile and extract to a folder. you can upload the rdl files the old fashion way or you can test drive the new version on my upload script.

To run the script you only need to specify the url to the reporting server(normally the site server)
The script will create a new folder called "Larsen Reports"(can be set with parameter), upload the reports and change the datasource to your configmgr server. 

& '.\Upload Reports v3.ps1' -webServiceUrl http://your-report-server 



Staring with the controls ,the "Deployments" field lists all task sequences and deployments in a readable format. Next the "Computer" will filter to only show computers that have run that deployment.

Lastly for the "Start Date" and "End Date" you just need to make sure the computers OS Deployment are between these dates. (These two parameters are mostly used by the Dashboard to select the correct deployment when there are several attempts for a single machine.) 

The top of the report shows some basic info about the machine, name ,DN and hardware model if they are available.



The part your most likely interested in is the step by step status for the OS Deployment.
It'll show you a nicely formatted (if i may say so myself:-) ) list  with the details for each step. in the task sequence and how long time each step took.



The list is quite minimalist ,but a lot of info is available as tooltips. Execution status on the Group And Action Name and Exit Code and Action Output on the Step number.
If a group or action is skipped the text is over lined to illustrate that. If an action has an exit code other than 0 (Success) the line is colored red so any errors should be easy to spot.



If you have any trouble displaying the graphics in the report ,try restarting the SSRS Service.

As always, hope you like the report. Post comments and questions below.
If you think others might find this usefull ,feel free to share a link on twitter ,facebook and forums. (this blog is tiny so not many people have seen this yet) 





Friday, April 1, 2016

OS Deployment Dashboard



Update 7 June: 
Have updated the download link with an updatec version(much faster) of the report.
If you downloaded the report before 7 June 2016 you should update to the newest version.
New version is also include the drill trough report (OS-OSD Details Report) so that you can click on a Computer to bring up the step by step details for that OS Deployment.

One of the first reports that i posted on this blog and by far the most popular is my "OSD - Overview" report. That report is now over 2 years old ,since then I've done a lot of custom report work both internally and for other clients and I've learned a lot of new techniques. My old OSD report was no longer up to my standard so it was time to create a new one.

Been working on this for a couple of days now and i think it's ready to show the public ..,so i present to you ,the new "Task Sequence Dashboard" ,hope  you like it!

Task Sequence Report ,main page

Download

First page is a dashboard like view ,while page two and up contains details.
Hopefully most of the elements in the report should be self explanatory ,but i'll quicklime run trough some of the features.

The report has rebuilt controls ,now you can select only the task sequences that you're interested in. when you've selected the task sequences you get a list of deployments for the selected task sequences for, default it selects all deployments.
Last parameter is to set how far back you want to go ,default it's set to 3 months.




The main page have 4 elements:

Top left is a bar chart showing the number of OS Deployments pr day for the selected period.

Top right is an overview of what the success rate of each of the task sequences is in percent. It only considers "success vs. failed"

Bottom left is a overview of the active deployments for each task sequence. It shows the average runtime for each task sequence ,Active Deployements(that have been used) and how many computers have ran a each deployment.

Bottom right is a quick overview of what errors the computers might have encountered.
It has tooltips both on the table and the pie chart ,so if you hover over the error code it gives some more information.




Page two (and up) is a details page showing the status for each computer that have run the task sequence. It shows start and stop time using the clients local time ,and calculates how long the computer used on the deployment.
(Note! if the client hasn't done a hardware scan it uses a default timezone attribute in the report. Default it's set to 120)


I've colored coded the status messages so you can easily spot computers that have problems. The failed statuses have a tooltip that decode the error message when you hover over it.
(same decoder as the console uses ,it's not great for task sequence errors.)
Last column is bar chart so you easily can spot computers that used long time on the OSD.

Hope you like the report ,if you do please spread the word and/or post a comment bellow.






Wednesday, March 23, 2016

How-to add Date filters to a report

This is a quick post to answer a question i got in the comments of the "add collection selection" post.
The question was ,how to add start date and end date, start time and end time?

It's fairly easy to do ,all you need is a date field and the Between clause.
In this demo i'm going to use the client activity field as an example.

SELECT TOP 100 A.name0,
               A.user_name0,
               B.lastactivetime,
               B.lastddr,
               lasthw,
               lastsw
FROM   v_r_system A
       JOIN v_ch_clientsummary B
         ON A.resourceid = B.resourceid
WHERE  lastactivetime BETWEEN '2016-03-20' AND '2016-03-23' 


In SSRS you might want to use parameters as the date values ,in my example i create a "From" and "To" Parameters and set their default values ,making the report more user friendly

The From Parameters default value is set to "=DateAdd("d",-30,Today())" that sets the default to 30 days back.



The To Parameters default value is set to "=Today()" ,setting it's default to today.
Change the last line in the query to use the parameters

WHERE  lastactivetime BETWEEN @From AND @To 

when i run the report the two parameters should be set by default to display clients that have been active in the last month.



Hope you found this usefull.

Tuesday, March 22, 2016

ConfigMgr Client Versions.(Updated to support up to 1602)

Have updated this report to identify client up to 1602 ,you can either replace the whole report or just update the "clientinfo" query

Hi ,it's been a while since my last post so i thought i post a small report I created to check that my clients are running the latest version.
Don't know about you ,but i don't memorize version numbers ,so i created this report that maps the version numbers to different ConfigMgr releases ,CU's and Service packs.



Download: ConfigMgr Versions.RDL
(Have updated the report to identify clients up to 1602)


Unfortunately I haven't found a way to automatically map the version numbers so it's a manual
process. This relase can map versions up to "Configmgr 1602" ,but it's easy to add more versions numbers if you need to...


Hope you find this useful.
Feedback is always nice ,so if you like it, find a problem or have an idea for improving the report ,post a comment bellow.

As Always ,here is the query for the report:
(As you can see it's nothing to it ,just a big case statement that maps the version number to the version name)


SELECT A.name0,
       A.user_name0,
       B.lastactivetime,
       B.lasthw,
       B.lastsw,
       C.caption0,
       A.ad_site_name0,
       A.client0,
       A.client_type0,
       A.client_version0,
       CASE
         WHEN A.client_version0 = '5.00.7711.0000' THEN
         'Configuration Manager RTM'
         WHEN A.client_version0 = '5.00.7711.0200' THEN
         'Configuration Manager CU1'
         WHEN A.client_version0 = '5.00.7711.0301' THEN
         'Configuration Manager CU2'
         WHEN A.client_version0 = '5.00.7804.1000' THEN
         'Configuration Manager SP1'
         WHEN A.client_version0 = '5.00.7804.1202' THEN
         'Configuration Manager SP1 CU1'
         WHEN A.client_version0 = '5.00.7804.1300' THEN
         'Configuration Manager SP1 CU2'
         WHEN A.client_version0 = '5.00.7804.1400' THEN
         'Configuration Manager SP1 CU3'
         WHEN A.client_version0 = '5.00.7804.1500' THEN
         'Configuration Manager SP1 CU4'
         WHEN A.client_version0 = '5.00.7804.1600' THEN
         'Configuration Manager SP1 CU5'
         WHEN A.client_version0 = '5.00.7958.1000' THEN
         'Configuration Manager R2'
         WHEN A.client_version0 = '5.00.7958.1101' THEN
         'Configuration Manager R2 Hotfix: KB 2905002'
         WHEN A.client_version0 = '5.00.7958.1203' THEN
         'Configuration Manager R2 CU1'
         WHEN A.client_version0 = '5.00.7958.1303' THEN
         'Configuration Manager R2 CU2'
         WHEN A.client_version0 = '5.00.7958.1401' THEN
         'Configuration Manager R2 CU3'
         WHEN A.client_version0 = '5.00.7958.1501' THEN
         'Configuration Manager R2 CU4'
         WHEN A.client_version0 = '5.00.7958.1604' THEN
         'Configuration Manager R2 CU5'
         WHEN A.client_version0 = '5.00.8239.1000' THEN
         'Configuration Manager R2 SP1'
         WHEN A.client_version0 = '5.00.8239.1203' THEN
         'Configuration Manager R2 SP1 CU1'
         WHEN A.client_version0 = '5.00.8239.1301' THEN
         'Configuration Manager R2 SP1 CU2'
         WHEN A.client_version0 = '5.00.8239.1403' THEN
         'Configuration Manager R2 SP1 CU3'
         WHEN A.client_version0 = '5.00.8325.1000' THEN
         'Configuration Manager 1511'
         WHEN A.client_version0 = '5.00.8355.1000' THEN
         'Configuration Manager 1602'
         ELSE 'Unknown'
       END AS Clientversion,
       A.creation_date0,
       A.deviceowner0,
       A.distinguished_name0,
       A.full_domain_name0,
       A.internetenabled0,
       A.is_assigned_to_user0
FROM   v_r_system A
       LEFT OUTER JOIN v_ch_clientsummary B
                    ON A.resourceid = B.resourceid
       LEFT OUTER JOIN v_gs_operating_system C
                    ON A.resourceid = C.resourceid
WHERE  client_type0 = 1
       AND A.resourceid IN (SELECT resourceid
                            FROM   dbo.v_fullcollectionmembership
                            WHERE  dbo.v_fullcollectionmembership.collectionid
                                   IN ( @Collections )) 


Thursday, January 7, 2016

SSRS Search Filter

Hi

This is just a quick post to show how you can create a search filter to limit a long list ,in this case software info collected by Configmgr.
Combine this with multi valued parameters and you have a good method to show all software versions, even if the vendor don't keep the publisher name consistant. 

In my case, if i didn't have the search filter the Publisher list would be 3338 entries long ,not very user friendly...


So what we do is to add a search filter that we use in the query that populates the Publisher Parameter. We take the input from the searchfilter and use it in the query for the next Publisher list with a LIKE statement (We use a replace statement to translate the wildcard to "%" )

SELECT DISTINCT publisher0 AS Publisher
FROM   v_add_remove_programs
WHERE  publisher0 LIKE Replace(@SearchFilter, '*', '%') 


With the use of multi valued parameters we can select the Publishers we want. Just remember with multi valued parameters you have to use the IN operator.
So to get the the values for the application list parameter we do the following.

SELECT DISTINCT displayname0 AS Displayname
FROM   v_add_remove_programs
WHERE  publisher0 IN ( @Publisher ) 


So now we can just create the main query with two IN Operators. Where Publisher is in the publisher list and Displayname in the Sofware List.

SELECT name0,
       B.*
FROM   v_r_system A
       JOIN v_add_remove_programs B
         ON A.resourceid = B.resourceid
WHERE  B.publisher0 IN ( @Publisher )
       AND B.displayname0 IN ( @Software ) 



Last ,build your report.
Here I've just created a simple tablix with result just to show you that it works:-)