Quantcast
Channel: Microsoft SQL Server Integration Services
Viewing all 149 articles
Browse latest View live

Deploying ispac files with PowerShell

$
0
0
Case
I have an Integration Services catalog with multiple projects and one generic environment. This environment contains all possible variables to support all projects.

Multiple project using the same generic environment














How do I deploy my ispac files with PowerShell and automatically create a reference to this generic environment. (All parameter in my project are available as variable in the environment, but not all variables in my generic environment are available as parameter in my project.)


Solution
You can use the following PowerShell scripts as a base. Feel free to post changes in the comments below. This example uses two scripts. The first one is a simple script where you only store parameter values:

  1. IspacFilePath is mandatory. Is contains the full path of the ispac file.
  2. SsisServer is mandatory. It contains the servername (and instance name) of the Sql Server that has the Integration Services catalog.
  3. FolderName is mandatory. It contains the name of the catalog folder. If it doesn't exist then it will be created
  4. ProjectName is optional. If it is empty the filename from the ispac file will be used as projectname. However, unfortunately it must be equal to the internal projectname of the ispac file. The ispac wizard is able to change the projectname, but that doesn't seem to be possible with PowerShell.
  5. EnvironmentName is optional. If it is empty then no environment will be referenced
  6. EnvironmentFolderName is optional. If it is empty then the script will search the environment in the project folder.
#PowerShell: finance.ps1
#################################################################################################
# Change source, destination and environment properties
#################################################################################################

# Source
$IspacFilePath = "d:\projects\Finance\bin\Development\Finance.ispac"

# Destination
$SsisServer =".\sql2016"
$FolderName = "Finance"
$ProjectName = ""

# Environment
$EnvironmentName = "Generic"
$EnvironmentFolderName = "Environments"

#################################################################################################
# Execute generic deployment script
.\generalDeployment.ps1 $IspacFilePath $SsisServer $FolderName $ProjectName $EnvironmentName $EnvironmentFolderName


The second script is the generic deployment script which is called by the first script. Developers only change the parameters in the first script and pass it through to the server administrator who executes it.

#PowerShell: generalDeployment.ps1
################################
########## PARAMETERS ##########
################################
[CmdletBinding()]
Param(
# IsPacFilePath is required
[Parameter(Mandatory=$True,Position=1)]
[string]$IspacFilePath,

# SsisServer is required
[Parameter(Mandatory=$True,Position=2)]
[string]$SsisServer,

# FolderName is required
[Parameter(Mandatory=$True,Position=3)]
[string]$FolderName,

# ProjectName is not required
# If empty filename is used
[Parameter(Mandatory=$False,Position=4)]
[string]$ProjectName,

# EnvironmentName is not required
# If empty no environment is referenced
[Parameter(Mandatory=$False,Position=5)]
[string]$EnvironmentName,

# EnvironmentFolderName is not required
# If empty the FolderName param is used
[Parameter(Mandatory=$False,Position=6)]
[string]$EnvironmentFolderName
)

# Replace empty projectname with filename
if (-not $ProjectName)
{
$ProjectName = [system.io.path]::GetFileNameWithoutExtension($IspacFilePath)
}
# Replace empty Environment folder with project folder
if (-not $EnvironmentFolderName)
{
$EnvironmentFolderName = $FolderName
}

clear
Write-Host "========================================================================================================================================================"
Write-Host "== Used parameters =="
Write-Host "========================================================================================================================================================"
Write-Host "Ispac File Path : " $IspacFilePath
Write-Host "SSIS Server : " $SsisServer
Write-Host "Project Folder Path : " $FolderName
Write-Host "Project Name : " $ProjectName
Write-Host "Environment Name : " $EnvironmentName
Write-Host "Environment Folder Path: " $EnvironmentFolderName
Write-Host "========================================================================================================================================================"
Write-Host ""

###########################
########## ISPAC ##########
###########################
# Check if ispac file exists
if (-Not (Test-Path $IspacFilePath))
{
Throw [System.IO.FileNotFoundException] "Ispac file $IspacFilePath doesn't exists!"
}
else
{
$IspacFileName = split-path $IspacFilePath -leaf
Write-Host "Ispac file" $IspacFileName "found"
}


############################
########## SERVER ##########
############################
# Load the Integration Services Assembly
Write-Host "Connecting to server $SsisServer "
$SsisNamespace = "Microsoft.SqlServer.Management.IntegrationServices"
[System.Reflection.Assembly]::LoadWithPartialName($SsisNamespace) | Out-Null;

# Create a connection to the server
$SqlConnectionstring = "Data Source=" + $SsisServer + ";Initial Catalog=master;Integrated Security=SSPI;"
$SqlConnection = New-Object System.Data.SqlClient.SqlConnection $SqlConnectionstring

# Create the Integration Services object
$IntegrationServices = New-Object $SsisNamespace".IntegrationServices" $SqlConnection

# Check if connection succeeded
if (-not $IntegrationServices)
{
Throw [System.Exception] "Failed to connect to server $SsisServer "
}
else
{
Write-Host "Connected to server" $SsisServer
}


#############################
########## CATALOG ##########
#############################
# Create object for SSISDB Catalog
$Catalog = $IntegrationServices.Catalogs["SSISDB"]

# Check if the SSISDB Catalog exists
if (-not $Catalog)
{
# Catalog doesn't exists. The user should create it manually.
# It is possible to create it, but that shouldn't be part of
# deployment of packages.
Throw [System.Exception] "SSISDB catalog doesn't exist. Create it manually!"
}
else
{
Write-Host "Catalog SSISDB found"
}


############################
########## FOLDER ##########
############################
# Create object to the (new) folder
$Folder = $Catalog.Folders[$FolderName]

# Check if folder already exists
if (-not $Folder)
{
# Folder doesn't exists, so create the new folder.
Write-Host "Creating new folder" $FolderName
$Folder = New-Object $SsisNamespace".CatalogFolder" ($Catalog, $FolderName, $FolderName)
$Folder.Create()
}
else
{
Write-Host "Folder" $FolderName "found"
}


#############################
########## PROJECT ##########
#############################
# Deploying project to folder
if($Folder.Projects.Contains($ProjectName)) {
Write-Host "Deploying" $ProjectName "to" $FolderName "(REPLACE)"
}
else
{
Write-Host "Deploying" $ProjectName "to" $FolderName "(NEW)"
}
# Reading ispac file as binary
[byte[]] $IspacFile = [System.IO.File]::ReadAllBytes($IspacFilePath)
$Folder.DeployProject($ProjectName, $IspacFile)
$Project = $Folder.Projects[$ProjectName]
if (-not $Project)
{
# Something went wrong with the deployment
# Don't continue with the rest of the script
return ""
}


#################################
########## ENVIRONMENT ##########
#################################
# Check if environment name is filled
if (-not $EnvironmentName)
{
# Kill connection to SSIS
$IntegrationServices = $null

# Stop the deployment script
Return "Ready deploying $IspacFileName without adding environment references"
}

# Create object to the (new) folder
$EnvironmentFolder = $Catalog.Folders[$EnvironmentFolderName]

# Check if environment folder exists
if (-not $EnvironmentFolder)
{
Throw [System.Exception] "Environment folder $EnvironmentFolderName doesn't exist"
}

# Check if environment exists
if(-not $EnvironmentFolder.Environments.Contains($EnvironmentName))
{
Throw [System.Exception] "Environment $EnvironmentName doesn't exist in $EnvironmentFolderName "
}
else
{
# Create object for the environment
$Environment = $Catalog.Folders[$EnvironmentFolderName].Environments[$EnvironmentName]

if ($Project.References.Contains($EnvironmentName, $EnvironmentFolderName))
{
Write-Host "Reference to" $EnvironmentName "found"
}
else
{
Write-Host "Adding reference to" $EnvironmentName
$Project.References.Add($EnvironmentName, $EnvironmentFolderName)
$Project.Alter()
}
}


########################################
########## PROJECT PARAMETERS ##########
########################################
$ParameterCount = 0
# Loop through all project parameters
foreach ($Parameter in $Project.Parameters)
{
# Get parameter name and check if it exists in the environment
$ParameterName = $Parameter.Name
if ($Environment.Variables.Contains($Parameter.Name))
{
$ParameterCount = $ParameterCount + 1
Write-Host "Project parameter" $ParameterName "connected to environment"
$Project.Parameters[$Parameter.Name].Set([Microsoft.SqlServer.Management.IntegrationServices.ParameterInfo+ParameterValueType]::Referenced, $Parameter.Name)
$Project.Alter()
}
else
{
# Variable with the name of the project parameter is not found in the environment
# Throw an exeception or remove next line to ignore parameter
Throw [System.Exception] "Project parameter $ParameterName doesn't exist in environment"
}
}
Write-Host "Number of project parameters mapped:" $ParameterCount


########################################
########## PACKAGE PARAMETERS ##########
########################################
$ParameterCount = 0
# Loop through all packages
foreach ($Package in $Project.Packages)
{
# Loop through all package parameters
foreach ($Parameter in $Package.Parameters)
{
# Get parameter name and check if it exists in the environment
$PackageName = $Package.Name
$ParameterName = $Parameter.Name
if ($Environment.Variables.Contains($Parameter.Name))
{
$ParameterCount = $ParameterCount + 1
Write-Host "Package parameter" $ParameterName "from package" $PackageName "connected to environment"
$Package.Parameters[$Parameter.Name].Set([Microsoft.SqlServer.Management.IntegrationServices.ParameterInfo+ParameterValueType]::Referenced, $Parameter.Name)
$Package.Alter()
}
else
{
# Variable with the name of the package parameter is not found in the environment
# Throw an exeception or remove next line to ignore parameter
Throw [System.Exception] "Package parameter $ParameterName from package $PackageName doesn't exist in environment"
}
}
}
Write-Host "Number of package parameters mapped:" $ParameterCount


###########################
########## READY ##########
###########################
# Kill connection to SSIS
$IntegrationServices = $null


Return "Ready deploying $IspacFileName "

The result
When executing the PowerShell script you get some feedback from the deployment process.
Executing PowerShell Deployment Script



















When finished the project will be deployed and all parameters are mapped to variables from the generic environment.
Parameters mapped















Renaming project during deployment Ispac file

$
0
0
Case
I have an SSIS project in Visual Studio which is called "01 StageFinance". The prefix is to preserve the order of projects in the Visual Studio solution.
Solution Explorer inVisual Studio












When I manually deploy the Ispac file to the Catalog, I have the possibility to rename the project from "01 StageFinance" to "StageFinance".

Possible to rename project in Wizard















But when I deploy the Ispac file via Powershell or TSQL then it returns an error when I rename the prject name: Failed to deploy the project. Fix the problems and try again later.:The specified project name, StageFinance, does not match the project name in the deployment file. Why do I need to provide the project name if I can't change it?

Solution
I used the SQL Server Profiler to see what happens during deployment. Both the Wizard and the PowerShell script call the stored procedure deploy_project

exec [SSISDB].[catalog].[deploy_project]
@folder_name=N'Finance',@project_name=N'StageFinance',@project_stream=0x504B0304



What struc to me was that the Wizard called the stored procedure with the new/changed name and then it didn't fail. And when I tried that with powershell it failed.

$NewProjectName = "StageFinance"
[byte[]] $IspacFile = [System.IO.File]::ReadAllBytes($IspacFilePath)
$Folder.DeployProject($NewProjectName, $IspacFile)

Conclusion: The ispac file was probably modified by the Wizard. Next I unzipped the Ispac file and edited the file @Project.manifest and saw that line 4 contained the project name.
@Project.manifest








I changed that project name to "StageFinance", zipped all the files and renamed the zipfile to .Ispac. Then I deployed the new Ispac file without any errors! Doing that manually each time the project needs to be deployed is annoying.

Automate rename with PowerShell
Recently I posted a PowerShell deployment script for automatic deployment. I added a sectio to that script to do the project rename:
# Partial Script
############################
########## RENAME ##########
############################
# If the filename and projectname are different
# Then we need to rename the internal projectname
# before deploying it.

# Derive projectname from ISPAC filename
$CurrentProjectName = [system.io.path]::GetFileNameWithoutExtension($IspacFilePath)

# Check if rename is necessary
If (-not($CurrentProjectName -eq $ProjectName))
{
# Split filepath of ispac file in folder and file
$TmpUnzipPath = split-path $IspacFilePath -Parent
# Determine the filepath of the new ispac file
$NewIspacFilePath = $TmpUnzipPath + "\" + $ProjectName + ".ispac"
# Determine the path of the unzip folder
$TmpUnzipPath = $TmpUnzipPath + "\" + $CurrentProjectName

# Catch unexpected errors and stop script
Try
{
# Check if new ispac already exists
if (Test-Path $NewIspacFilePath)
{
[System.IO.File]::Delete($NewIspacFilePath)
}

# Search strings
$SearchStringStart = ''
$SearchStringEnd = '
'

# Add reference to compression namespace
Add-Type -assembly "system.io.compression.filesystem"

# Extract ispac file to temporary location (.NET Framework 4.5)
[io.compression.zipfile]::ExtractToDirectory($IspacFilePath, $TmpUnzipPath)

# Replace internal projectname with new projectname
$EditFile = $TmpUnzipPath + "\@Project.manifest"
(get-content $EditFile).replace($SearchStringStart + $CurrentProjectName + $SearchStringEnd, $SearchStringStart + $ProjectName + $SearchStringEnd) | set-content $EditFile

# Zip temporary location to new ispac file (.NET Framework 4.5)
[io.compression.zipfile]::CreateFromDirectory($TmpUnzipPath, $NewIspacFilePath)

# Delete temporary location
[System.IO.Directory]::Delete($TmpUnzipPath, $True)

# Replace ispac parameter
$IspacFilePath = $NewIspacFilePath
}
Catch [System.Exception]
{
Throw [System.Exception] "Failed renaming project in $IspacFileName : $_.Exception.Message "
}
}

The complete script can be downloaden here.



Thanks to Bill Fellows for pointing me in the right direction.

Deploying environments and variables with PowerShell (A: array)

$
0
0
Case
I want to deploy an environment with variables to the Integration Services Catalog. How do I do that?
Environment with variables














Solution
You can't create them outside SQL Server Management Studio and then deploy them to the catalog, because the environment isn't part of the ISPAC file. But it's possible to generate some TSQL code and then execute those Store Procedure calls on all environments of the DTAP.
Or you can use PowerShell script to generate an environment with variables. This blog post shows three different options to use PowerShell to create environments:
  1. Store values in (multi-)array
  2. Store values in database table
  3. Store values in CSV file
A: Store values in (multi-)array
This first example works with a single powershell file with 4 adjustable parameters on top. The variable names and values for this example are stored in an array within the script.

A list of all parameters:


  • SsisServer contains the servername (and instance name) of the Sql Server that has the Integration Services catalog.
  • EnvironmentName contains the name of the environment. If the environment already exists then it just adds or updates variables. There are deliberately no deletes. but that's possible.
  • EnvironmentFolderName contains the foldername where the environment is located. If the folder doesn't exists, it will be created.
  • EnvironmentVars is an array of variables with the datatype, name, value, description and an indication whether it is a sensitive parameters. The example uses String, Int16 and Boolean, but there are other datatypes possible like DataTime, Int32, Int64, etc. You could store these variables in a database table or an excel sheet and then use some string concatenation to generate the PowerShell code lines for this array.


  • The script contains a lot of feedback. If it's to excessive for you, you can skip a couple of Write-Host lines by adding a hashtag in front of it. For deploying it's not necessary to change any lines below the parameters.


    #PowerShell: GeneralEnvironmentDeploymentWithArray.ps1
    ################################
    ########## PARAMETERS ##########
    ################################
    # Change Server, enviroment folder and enviroment name
    $SsisServer = "localhost"
    $EnvironmentFolderName = "Environments"
    $EnvironmentName = "GenericEnv"

    # Change Variables (mind the commas at the end, not on last line)
    # Columns: Datatype, Name, Value, Description, Sensitive
    $EnvironmentVars = @(
    ("String","MIS_STG_Connectionstring","Data Source=.\sql2016;Initial Catalog=MIS_STG;Provider=SQLNCLI11.1;Integrated Security=SSPI;Auto Translate=False;","Connectionstring to stage database",$false),
    ("String","MIS_HST_Connectionstring","Data Source=.\sql2016;Initial Catalog=MIS_HST;Provider=SQLNCLI11.1;Integrated Security=SSPI;Auto Translate=False;","Connectionstring to historical stage database",$false),
    ("String","MIS_DWH_Connectionstring","Data Source=.\sql2016;Initial Catalog=MIS_DWH;Provider=SQLNCLI11.1;Integrated Security=SSPI;Auto Translate=False;","Connectionstring to data warehouse database",$false),
    ("String","MIS_MTA_Connectionstring","Data Source=.\sql2016;Initial Catalog=MIS_MTA;Provider=SQLNCLI11.1;Integrated Security=SSPI;Auto Translate=False;","Connectionstring to metadata database",$false),
    ("String","MIS_DM_Connectionstring","Data Source=.\sql2016;Initial Catalog=MIS_DM;Provider=SQLNCLI11.1;Integrated Security=SSPI;Auto Translate=False;","Connectionstring to data mart database",$false),
    ("String","FtpPassword","53cr3t!","Secret FTP password",$true),
    ("String","FtpUser","SSISJoost","Username for FTP",$false),
    ("String","FtpServer","ftp://SSISJoost.nl","FTP Server",$false),
    ("String","FolderStageFiles","d:\sources\","Location of stage files",$false),
    ("Boolean","EnableRetry",$true,"Enable retry for Webservice Task",$false),
    ("Int16","NumberOfRetries", 3,"Number of retries for Webservice Task",$false),
    ("Int16","SecsPauseBetweenRetry", 30,"Number of seconds between retry",$false)
    )

    #################################################
    ########## DO NOT EDIT BELOW THIS LINE ##########
    #################################################
    clear
    Write-Host "========================================================================================================================================================"
    Write-Host "== Used parameters =="
    Write-Host "========================================================================================================================================================"
    Write-Host "SSIS Server :" $SsisServer
    Write-Host "Environment Name :" $EnvironmentName
    Write-Host "Environment Folder Path :" $EnvironmentFolderName
    Write-Host "Number of Variables :" $EnvironmentVars.Count
    Write-Host "========================================================================================================================================================"


    ############################
    ########## SERVER ##########
    ############################
    # Load the Integration Services Assembly
    Write-Host "Connecting to server $SsisServer "
    $SsisNamespace = "Microsoft.SqlServer.Management.IntegrationServices"
    [System.Reflection.Assembly]::LoadWithPartialName($SsisNamespace) | Out-Null;

    # Create a connection to the server
    $SqlConnectionstring = "Data Source=" + $SsisServer + ";Initial Catalog=master;Integrated Security=SSPI;"
    $SqlConnection = New-Object System.Data.SqlClient.SqlConnection $SqlConnectionstring

    # Create the Integration Services object
    $IntegrationServices = New-Object $SsisNamespace".IntegrationServices" $SqlConnection

    # Check if connection succeeded
    if (-not $IntegrationServices)
    {
    Throw [System.Exception] "Failed to connect to server $SsisServer "
    }
    else
    {
    Write-Host "Connected to server" $SsisServer
    }


    #############################
    ########## CATALOG ##########
    #############################
    # Create object for SSISDB Catalog
    $Catalog = $IntegrationServices.Catalogs["SSISDB"]

    # Check if the SSISDB Catalog exists
    if (-not $Catalog)
    {
    # Catalog doesn't exists. The user should create it manually.
    # It is possible to create it, but that shouldn't be part of
    # deployment of packages or environments.
    Throw [System.Exception] "SSISDB catalog doesn't exist. Create it manually!"
    }
    else
    {
    Write-Host "Catalog SSISDB found"
    }


    ############################
    ########## FOLDER ##########
    ############################
    # Create object to the (new) folder
    $Folder = $Catalog.Folders[$EnvironmentFolderName]

    # Check if folder already exists
    if (-not $Folder)
    {
    # Folder doesn't exists, so create the new folder.
    Write-Host "Creating new folder" $EnvironmentFolderName
    $Folder = New-Object $SsisNamespace".CatalogFolder" ($Catalog, $EnvironmentFolderName, $EnvironmentFolderName)
    $Folder.Create()
    }
    else
    {
    Write-Host "Folder" $EnvironmentFolderName "found"
    }


    #################################
    ########## ENVIRONMENT ##########
    #################################
    # Create object for the (new) environment
    $Environment = $Catalog.Folders[$EnvironmentFolderName].Environments[$EnvironmentName]

    # Check if folder already exists
    if (-not $Environment)
    {
    Write-Host "Creating environment" $EnvironmentName in $EnvironmentFolderName

    $Environment = New-Object $SsisNamespace".EnvironmentInfo" ($Folder, $EnvironmentName, $EnvironmentName)
    $Environment.Create()
    }
    else
    {
    Write-Host "Environment" $EnvironmentName "found with" $Environment.Variables.Count "existing variables"
    # Optional: Recreate to delete all variables, but be careful:
    # This could be harmful for existing references between vars and pars
    # if a used variable is deleted and not recreated.
    #$Environment.Drop()
    #$Environment = New-Object $SsisNamespace".EnvironmentInfo" ($folder, $EnvironmentName, $EnvironmentName)
    #$Environment.Create()
    }


    ###############################
    ########## VARIABLES ##########
    ###############################
    $InsertCount = 0
    $UpdateCount = 0

    # Loop through the (multi-)array EnvironmentVars that contains all variables
    for($i=0
    $i -le $EnvironmentVars.Count-1
    $i++)
    {
    # Get variablename from array and try to find it in the environment
    $Variable = $Catalog.Folders[$EnvironmentFolderName].Environments[$EnvironmentName].Variables[$EnvironmentVars[$i][1]]

    # Check if the variable exists
    if (-not $Variable)
    {
    # Insert new variable
    Write-Host "Variable" $EnvironmentVars[$i][1] "added"
    $Environment.Variables.Add($EnvironmentVars[$i][1], $EnvironmentVars[$i][0], $EnvironmentVars[$i][2], $EnvironmentVars[$i][4], $EnvironmentVars[$i][3])

    $InsertCount = $InsertCount + 1
    }
    else
    {
    # Update existing variable
    Write-Host "Variable" $EnvironmentVars[$i][1] "updated"
    $Variable.Type = $EnvironmentVars[$i][0]
    $Variable.Value = $EnvironmentVars[$i][2]
    $Variable.Description = $EnvironmentVars[$i][3]
    $Variable.Sensitive = $EnvironmentVars[$i][4]

    $UpdateCount = $UpdateCount + 1
    }
    }
    $Environment.Alter()


    ###########################
    ########## READY ##########
    ###########################
    # Kill connection to SSIS
    $IntegrationServices = $null
    Write-Host "Finished, total inserts" $InsertCount " and total updates" $UpdateCount


    New environment

    Existing environment





























    Download scripts

    Tip: if you keep the parameter names in your SSIS projects equal to the variable names in the environment, then you could automatically references them during deployment using this PowerShell script.

    Deploying environments and variables with PowerShell (B: table)

    $
    0
    0
    Case
    I want to deploy an environment with variables to the Integration Services Catalog. How do I do that?
    Environment with variables














    Solution
    You can't create them outside SQL Server Management Studio and then deploy them to the catalog. The environment isn't part of the ISPAC file. But it's possible to generate some TSQL code and then execute those Store Procedure calls on all environments of the DTAP.
    Or you can use PowerShell script to generate an environment with variables. This blog post shows three different options to use PowerShell to create environments:
    1. Store values in (multi-)array
    2. Store values in database table
    3. Store values in CSV file
    B: Store values in database table
    This second example works with two powershell files. The first only contains five parameters. The second file contains the real logic and is called by the first. A benefit of two separate files is that you can use the scripts for multiple environments, but all logic is stored in a single file. In this example the variables are stored in a table instead of an array. An array can't be used as parameter between two separate files.

    A list of all parameters:


  • SsisServer contains the servername (and instance name) of the Sql Server that has the Integration Services catalog.
  • EnvironmentName contains the name of the environment. If the environment already exists then it just adds or updates variables. There are deliberately no deletes. but that's possible.
  • EnvironmentFolderName contains the foldername where the environment is located. If the folder doesn't exists, it will be created.
  • SqlServer contains the servername (and instance name) of the Sql Server that has a database with the table containing the variables. Note that this server should be reachable from all environments where you want to run this PowerShell script.
  • SqlDatabase contains the database name that has the table containing the variables. 

  • This example uses strings, Int16 and Boolean, but there are other datatypes like DataTime, Int32, Int64, etc.). The script contains a lot of feedback. If it's to excessive for you, you can skip a couple of Write-Host lines by adding a hashtag in front of it. For deploying it's not necessary to change anything in the second file.


    #PowerShell: ParamsForGeneralEnvironmentDeploymentWithTable.ps1
    #################################################################################################
    # Change source and destination properties
    #################################################################################################
    # Ssis
    $SsisServer ="localhost"
    $EnvironmentFolderName = "Environments"
    $EnvironmentName = "Generic"

    # SqlServer (config table)
    $SqlServer ="localhost"
    $SqlDatabase ="ssisjoost"

    # Execute deployment script
    . "$PSScriptRoot\GeneralEnvironmentDeploymentWithTable.ps1" $SsisServer $EnvironmentFolderName $EnvironmentName $SqlServer $SqlDatabase



    Second file:


    #PowerShell: GeneralEnvironmentDeploymentWithTable.ps1
    ################################
    ########## PARAMETERS ##########
    ################################
    [CmdletBinding()]
    Param(
    # SsisServer is required
    [Parameter(Mandatory=$True,Position=1)]
    [string]$SsisServer,

    # EnvironmentFolderName is required
    [Parameter(Mandatory=$True,Position=2)]
    [string]$EnvironmentFolderName,

    # EnvironmentName is required
    [Parameter(Mandatory=$True,Position=3)]
    [string]$EnvironmentName,

    # SqlServer is required
    [Parameter(Mandatory=$True,Position=4)]
    [string]$SqlServer,

    # SqlDatabase is required
    [Parameter(Mandatory=$True,Position=5)]
    [string]$SqlDatabase
    )

    clear
    Write-Host "========================================================================================================================================================"
    Write-Host "== Used parameters =="
    Write-Host "========================================================================================================================================================"
    Write-Host "SSIS Server :" $SsisServer
    Write-Host "Environment Name :" $EnvironmentName
    Write-Host "Environment Folder Path :" $EnvironmentFolderName
    Write-Host "Sql Server (for config) :" $SqlServer
    Write-Host "Database (for config) :" $SqlDatabase
    Write-Host "========================================================================================================================================================"


    ###############################
    ########## VARIABLES ##########
    ###############################
    # Get variables from table

    # Load SqlServer.SMO assembly to get data out of SQL Server
    Write-Host "Connecting to SSIS server $SqlServer "
    Add-Type -AssemblyName "Microsoft.SqlServer.SMO, Version=12.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91"

    # Create SQL Server object using integrated security
    $SqlServerConf = New-Object Microsoft.SqlServer.Management.Smo.Server $SqlServer

    # Check if connection succeeded
    if (-not $SqlServerConf)
    {
    Throw [System.Exception] "Failed to connect to SQL server $SqlServer "
    }
    else
    {
    Write-Host "Connected to SQL server" $SqlServer
    }

    # Specify which database to use for the query
    $SqlDatabaseConf = $SqlServerConf.Databases.Item($SqlDatabase)
    # Specify query to get parameters out of helper table
    $SqlQueryConf = "SELECT Datatype, ParameterName, ParameterValue, ParameterDescription, Sensitive FROM EnvironmentVariables"
    # Execute query an put the result in a table object
    $TableConf = $SqlDatabaseConf.ExecuteWithResults($SqlQueryConf).Tables[0]

    Write-Host "Found" $TableConf.Rows.Count "variables in config table"


    ############################
    ########## SERVER ##########
    ############################
    # Load the Integration Services Assembly
    Write-Host "Connecting to SSIS server $SsisServer "
    $SsisNamespace = "Microsoft.SqlServer.Management.IntegrationServices"
    [System.Reflection.Assembly]::LoadWithPartialName($SsisNamespace) | Out-Null;

    # Create a connection to the server
    $SqlConnectionstring = "Data Source=" + $SsisServer + ";Initial Catalog=master;Integrated Security=SSPI;"
    $SqlConnection = New-Object System.Data.SqlClient.SqlConnection $SqlConnectionstring

    # Create the Integration Services object
    $IntegrationServices = New-Object $SsisNamespace".IntegrationServices" $SqlConnection

    # Check if connection succeeded
    if (-not $IntegrationServices)
    {
    Throw [System.Exception] "Failed to connect to SSIS server $SsisServer "
    }
    else
    {
    Write-Host "Connected to SSIS server" $SsisServer
    }


    #############################
    ########## CATALOG ##########
    #############################
    # Create object for SSISDB Catalog
    $Catalog = $IntegrationServices.Catalogs["SSISDB"]

    # Check if the SSISDB Catalog exists
    if (-not $Catalog)
    {
    # Catalog doesn't exists. The user should create it manually.
    # It is possible to create it, but that shouldn't be part of
    # deployment of packages or environments
    Throw [System.Exception] "SSISDB catalog doesn't exist. Create it manually!"
    }
    else
    {
    Write-Host "Catalog SSISDB found"
    }


    ############################
    ########## FOLDER ##########
    ############################
    # Create object to the (new) folder
    $Folder = $Catalog.Folders[$EnvironmentFolderName]

    # Check if folder already exists
    if (-not $Folder)
    {
    # Folder doesn't exists, so create the new folder.
    Write-Host "Creating new folder" $EnvironmentFolderName
    $Folder = New-Object $SsisNamespace".CatalogFolder" ($Catalog, $EnvironmentFolderName, $EnvironmentFolderName)
    $Folder.Create()
    }
    else
    {
    Write-Host "Folder" $EnvironmentFolderName "found"
    }


    #################################
    ########## ENVIRONMENT ##########
    #################################
    # Create object for the (new) environment
    $Environment = $Catalog.Folders[$EnvironmentFolderName].Environments[$EnvironmentName]

    # Check if folder already exists
    if (-not $Environment)
    {
    Write-Host "Creating environment" $EnvironmentName in $EnvironmentFolderName

    $Environment = New-Object $SsisNamespace".EnvironmentInfo" ($Folder, $EnvironmentName, $EnvironmentName)
    $Environment.Create()
    }
    else
    {
    Write-Host "Environment" $EnvironmentName "found with" $Environment.Variables.Count "existing variables"
    # Recreate to delete all variables, but be careful:
    # This could be harmful for existing references between vars and pars
    # if a used variable is deleted and not recreated.
    #$Environment.Drop()
    #$Environment = New-Object $SsisNamespace".EnvironmentInfo" ($folder, $EnvironmentName, $EnvironmentName)
    #$Environment.Create()
    }


    ###############################
    ########## VARIABLES ##########
    ###############################
    $InsertCount = 0
    $UpdateCount = 0


    foreach ($Row in $TableConf)
    {
    # Get variablename from array and try to find it in the environment
    $Variable = $Catalog.Folders[$EnvironmentFolderName].Environments[$EnvironmentName].Variables[$Row.Item("ParameterName")]

    # Check if the variable exists
    if (-not $Variable)
    {
    # Insert new variable
    Write-Host "Variable" $Row.Item("ParameterName") "added"
    $Environment.Variables.Add($Row.Item("ParameterName"), $Row.Item("Datatype"), $Row.Item("ParameterValue"), $Row.Item("Sensitive"), $Row.Item("ParameterDescription"))

    $InsertCount = $InsertCount + 1
    }
    else
    {
    # Update existing variable
    Write-Host "Variable" $Row.Item("ParameterName") "updated"
    $Variable.Type = $Row.Item("Datatype")
    $Variable.Value = $Row.Item("ParameterValue")
    $Variable.Description = $Row.Item("ParameterDescription")
    $Variable.Sensitive = $Row.Item("Sensitive")

    $UpdateCount = $UpdateCount + 1
    }
    }
    $Environment.Alter()


    ###########################
    ########## READY ##########
    ###########################
    # Kill connection to SSIS
    $IntegrationServices = $null
    Write-Host "Finished, total inserts" $InsertCount " and total updates" $UpdateCount



    The table create script and some insert statements to fill the table with sample data.


    --TSQL: CreateTableEnvironmentVariables.sql
    USE [ssisjoost]
    GO

    /****** Generate Table [dbo].[EnvironmentVariables] ******/
    SET ANSI_NULLS ON
    GO

    SET QUOTED_IDENTIFIER ON
    GO

    SET ANSI_PADDING ON
    GO

    CREATE TABLE [dbo].[EnvironmentVariables](
    [Datatype] [varchar](50) NULL,
    [ParameterName] [varchar](50) NULL,
    [ParameterValue] [varchar](255) NULL,
    [ParameterDescription] [varchar](255) NULL,
    [Sensitive] [bit] NULL
    ) ON [PRIMARY]

    GO

    SET ANSI_PADDING OFF
    GO
    -- Insert Test data
    INSERT [dbo].[EnvironmentVariables] ([Datatype], [ParameterName], [ParameterValue], [ParameterDescription], [Sensitive]) VALUES (N'String', N'MIS_STG_ConnectionString', N'Data Source=.\sql2016;Initial Catalog=MIS_STG;Provider=SQLNCLI11.1;Integrated Security=SSPI;Auto Translate=False;', N'Connectionstring to stage database', 0)
    GO
    INSERT [dbo].[EnvironmentVariables] ([Datatype], [ParameterName], [ParameterValue], [ParameterDescription], [Sensitive]) VALUES (N'String', N'MIS_HST_ConnectionString', N'Data Source=.\sql2016;Initial Catalog=MIS_HST;Provider=SQLNCLI11.1;Integrated Security=SSPI;Auto Translate=False;', N'Connectionstring to historical stage database', 0)
    GO
    INSERT [dbo].[EnvironmentVariables] ([Datatype], [ParameterName], [ParameterValue], [ParameterDescription], [Sensitive]) VALUES (N'String', N'MIS_DWH_ConnectionString', N'Data Source=.\sql2016;Initial Catalog=MIS_DWH;Provider=SQLNCLI11.1;Integrated Security=SSPI;Auto Translate=False;', N'Connectionstring to data warehouse database', 0)
    GO
    INSERT [dbo].[EnvironmentVariables] ([Datatype], [ParameterName], [ParameterValue], [ParameterDescription], [Sensitive]) VALUES (N'String', N'MIS_MTA_ConnectionString', N'Data Source=.\sql2016;Initial Catalog=MIS_MTA;Provider=SQLNCLI11.1;Integrated Security=SSPI;Auto Translate=False;', N'Connectionstring to metadata database', 0)
    GO
    INSERT [dbo].[EnvironmentVariables] ([Datatype], [ParameterName], [ParameterValue], [ParameterDescription], [Sensitive]) VALUES (N'String', N'MIS_DM_ConnectionString', N'Data Source=.\sql2016;Initial Catalog=MIS_DM;Provider=SQLNCLI11.1;Integrated Security=SSPI;Auto Translate=False;', N'Connectionstring to data mart database', 0)
    GO
    INSERT [dbo].[EnvironmentVariables] ([Datatype], [ParameterName], [ParameterValue], [ParameterDescription], [Sensitive]) VALUES (N'String', N'FtpPassword', N'53cr3t!', N'Secret FTP password', 1)
    GO
    INSERT [dbo].[EnvironmentVariables] ([Datatype], [ParameterName], [ParameterValue], [ParameterDescription], [Sensitive]) VALUES (N'String', N'FtpUser', N'SSISJoost', N'Username for FTP', 0)
    GO
    INSERT [dbo].[EnvironmentVariables] ([Datatype], [ParameterName], [ParameterValue], [ParameterDescription], [Sensitive]) VALUES (N'String', N'FtpServer', N'ftp://SSISJoost.nl', N'FTP Server', 0)
    GO
    INSERT [dbo].[EnvironmentVariables] ([Datatype], [ParameterName], [ParameterValue], [ParameterDescription], [Sensitive]) VALUES (N'String', N'FolderStageFiles', N'd:\sources\', N'Location of stage files', 0)
    GO
    INSERT [dbo].[EnvironmentVariables] ([Datatype], [ParameterName], [ParameterValue], [ParameterDescription], [Sensitive]) VALUES (N'Boolean', N'EnableRetry', N'true', N'Enable retry for Webservice Task', 0)
    GO
    INSERT [dbo].[EnvironmentVariables] ([Datatype], [ParameterName], [ParameterValue], [ParameterDescription], [Sensitive]) VALUES (N'Int16', N'NumberOfRetries', N'3', N'Number of retries for Webservice Task', 0)
    GO
    INSERT [dbo].[EnvironmentVariables] ([Datatype], [ParameterName], [ParameterValue], [ParameterDescription], [Sensitive]) VALUES (N'Int16', N'SecsPauseBetweenRetry', N'30', N'Number of seconds between retry', 0)
    GO


    New environment

    Existing environment





























    Download scripts

    Tip: if you keep the parameter names in your SSIS projects equal to the variable names in the environment, then you could automatically references them during deployment using this PowerShell script.

    Deploying environments and variables with PowerShell (C: csv)

    $
    0
    0
    Case
    I want to deploy an environment with variables to the Integration Services Catalog. How do I do that?
    Environment with variables














    Solution
    You can't create them outside SQL Server Management Studio and then deploy them to the catalog. The environment isn't part of the ISPAC file. But it's possible to generate some TSQL code and then execute those Store Procedure calls on all environments of the DTAP.
    Or you can use PowerShell script to generate an environment with variables. This blog post shows three different options to use PowerShell to create environments:
    1. Store values in (multi-)array
    2. Store values in database table
    3. Store values in CSV file
    C: Store values in CSV file
    This example works with two powershell files. The first only contains four parameters. The second file contains the real logic and is called by the first. A benefit of two separate files is that you can use the scripts for multiple environments, but all logic is stored in a single file. In this example the variables are stored in a CSV file instead of an array or database table. You can of course still store the values in a central database and then export the records to a CSV file for the PowerShell script.

    A list of all parameters:


  • SsisServer contains the servername (and instance name) of the Sql Server that has the Integration Services catalog.
  • EnvironmentName contains the name of the environment. If the environment already exists then it just adds or updates variables. There are deliberately no deletes. but that's possible.
  • EnvironmentFolderName contains the foldername where the environment is located. If the folder doesn't exists, it will be created.
  • FilepathCsv contains the path of the CSV file. This example uses $PSScriptRoot to get the location of the current PowerShell file and then concatenates the filename of the CSV to it. You can also use a regular path like d:\folder\EnvironmentVariables.csv.


  • This example uses strings, Int16 and Boolean, but there are other datatypes like DataTime, Int32, Int64, etc.). The script contains a lot of feedback. If it's to excessive for you, you can skip a couple of Write-Host lines by adding a hashtag in front of it. For deploying it's not necessary to change anything in the second file.


    #PowerShell: ParamsForGeneralEnvironmentDeploymentWithCsv.ps1
    #################################################################################################
    # Change source and destination properties
    #################################################################################################
    # Ssis
    $SsisServer ="."
    $EnvironmentFolderName = "Environments"
    $EnvironmentName = "Generic"

    # Path of CSV containing variables (you can also use format d:\file.csv)
    $FilepathCsv = "$PSScriptRoot\EnvironmentVariables.csv"

    # Execute deployment script
    . "$PSScriptRoot\GeneralEnvironmentDeploymentWithCsv.ps1" $SsisServer $EnvironmentFolderName $EnvironmentName $FilepathCsv



    Second file:


    #PowerShell: GeneralEnvironmentDeploymentWithCsv.ps1
    ################################
    ########## PARAMETERS ##########
    ################################
    [CmdletBinding()]
    Param(
    # SsisServer is required
    [Parameter(Mandatory=$True,Position=1)]
    [string]$SsisServer,

    # EnvironmentFolderName is required
    [Parameter(Mandatory=$True,Position=2)]
    [string]$EnvironmentFolderName,

    # EnvironmentName is required
    [Parameter(Mandatory=$True,Position=3)]
    [string]$EnvironmentName,

    # FilepathCsv is required
    [Parameter(Mandatory=$True,Position=4)]
    [string]$FilepathCsv
    )

    clear
    Write-Host "========================================================================================================================================================"
    Write-Host "== Used parameters =="
    Write-Host "========================================================================================================================================================"
    Write-Host "SSIS Server :" $SsisServer
    Write-Host "Environment Name :" $EnvironmentName
    Write-Host "Environment Folder Path :" $EnvironmentFolderName
    Write-Host "Filepath of CSV file :" $FilepathCsv
    Write-Host "========================================================================================================================================================"


    #########################
    ########## CSV ##########
    #########################
    # Check if ispac file exists
    if (-Not (Test-Path $FilepathCsv))
    {
    Throw [System.IO.FileNotFoundException] "CSV file $FilepathCsv doesn't exists!"
    }
    else
    {
    $FileNameCsv = split-path $FilepathCsv -leaf
    Write-Host "CSV file" $FileNameCsv "found"
    }


    ############################
    ########## SERVER ##########
    ############################
    # Load the Integration Services Assembly
    Write-Host "Connecting to SSIS server $SsisServer "
    $SsisNamespace = "Microsoft.SqlServer.Management.IntegrationServices"
    [System.Reflection.Assembly]::LoadWithPartialName($SsisNamespace) | Out-Null;

    # Create a connection to the server
    $SqlConnectionstring = "Data Source=" + $SsisServer + ";Initial Catalog=master;Integrated Security=SSPI;"
    $SqlConnection = New-Object System.Data.SqlClient.SqlConnection $SqlConnectionstring

    # Create the Integration Services object
    $IntegrationServices = New-Object $SsisNamespace".IntegrationServices" $SqlConnection

    # Check if connection succeeded
    if (-not $IntegrationServices)
    {
    Throw [System.Exception] "Failed to connect to SSIS server $SsisServer "
    }
    else
    {
    Write-Host "Connected to SSIS server" $SsisServer
    }


    #############################
    ########## CATALOG ##########
    #############################
    # Create object for SSISDB Catalog
    $Catalog = $IntegrationServices.Catalogs["SSISDB"]

    # Check if the SSISDB Catalog exists
    if (-not $Catalog)
    {
    # Catalog doesn't exists. The user should create it manually.
    # It is possible to create it, but that shouldn't be part of
    # deployment of packages or environments.
    Throw [System.Exception] "SSISDB catalog doesn't exist. Create it manually!"
    }
    else
    {
    Write-Host "Catalog SSISDB found"
    }


    ############################
    ########## FOLDER ##########
    ############################
    # Create object to the (new) folder
    $Folder = $Catalog.Folders[$EnvironmentFolderName]

    # Check if folder already exists
    if (-not $Folder)
    {
    # Folder doesn't exists, so create the new folder.
    Write-Host "Creating new folder" $EnvironmentFolderName
    $Folder = New-Object $SsisNamespace".CatalogFolder" ($Catalog, $EnvironmentFolderName, $EnvironmentFolderName)
    $Folder.Create()
    }
    else
    {
    Write-Host "Folder" $EnvironmentFolderName "found"
    }


    #################################
    ########## ENVIRONMENT ##########
    #################################
    # Create object for the (new) environment
    $Environment = $Catalog.Folders[$EnvironmentFolderName].Environments[$EnvironmentName]

    # Check if folder already exists
    if (-not $Environment)
    {
    Write-Host "Creating environment" $EnvironmentName in $EnvironmentFolderName

    $Environment = New-Object $SsisNamespace".EnvironmentInfo" ($Folder, $EnvironmentName, $EnvironmentName)
    $Environment.Create()
    }
    else
    {
    Write-Host "Environment" $EnvironmentName "found with" $Environment.Variables.Count "existing variables"
    # Optional: Recreate to delete all variables, but be careful:
    # This could be harmful for existing references between vars and pars
    # if a used variable is deleted and not recreated.
    #$Environment.Drop()
    #$Environment = New-Object $SsisNamespace".EnvironmentInfo" ($folder, $EnvironmentName, $EnvironmentName)
    #$Environment.Create()
    }


    ###############################
    ########## VARIABLES ##########
    ###############################
    $InsertCount = 0
    $UpdateCount = 0


    Import-CSV $FilepathCsv -Header Datatype,ParameterName,ParameterValue,ParameterDescription,Sensitive -Delimiter ';' | Foreach-Object{
    If (-not($_.Datatype -eq "Datatype"))
    {
    #Write-Host $_.Datatype "|" $_.ParameterName "|" $_.ParameterValue "|" $_.ParameterDescription "|" $_.Sensitive
    # Get variablename from array and try to find it in the environment
    $Variable = $Catalog.Folders[$EnvironmentFolderName].Environments[$EnvironmentName].Variables[$_.ParameterName]


    # Check if the variable exists
    if (-not $Variable)
    {
    # Insert new variable
    Write-Host "Variable" $_.ParameterName "added"
    $Environment.Variables.Add($_.ParameterName, $_.Datatype, $_.ParameterValue, [System.Convert]::ToBoolean($_.Sensitive), $_.ParameterDescription)

    $InsertCount = $InsertCount + 1
    }
    else
    {
    # Update existing variable
    Write-Host "Variable" $_.ParameterName "updated"
    $Variable.Type = $_.Datatype
    $Variable.Value = $_.ParameterValue
    $Variable.Description = $_.ParameterDescription
    $Variable.Sensitive = [System.Convert]::ToBoolean($_.Sensitive)

    $UpdateCount = $UpdateCount + 1
    }
    }
    }
    $Environment.Alter()


    ###########################
    ########## READY ##########
    ###########################
    # Kill connection to SSIS
    $IntegrationServices = $null
    Write-Host "Finished, total inserts" $InsertCount " and total updates" $UpdateCount


    CSV file:



    CSV: EnvironmentVariables.csv
    Datatype;Parameter Name;Parameter Value;Parameter Description;Sensitive (true or false)
    String;MIS_STG_ConnectionString;"Data Source=.\sql2016;Initial Catalog=MIS_STG;Provider=SQLNCLI11.1;Integrated Security=SSPI;Auto Translate=False;";Connectionstring to stage database;false
    String;MIS_HST_ConnectionString;"Data Source=.\sql2016;Initial Catalog=MIS_HST;Provider=SQLNCLI11.1;Integrated Security=SSPI;Auto Translate=False;";Connectionstring to historical stage database;false
    String;MIS_DWH_ConnectionString;"Data Source=.\sql2016;Initial Catalog=MIS_DWH;Provider=SQLNCLI11.1;Integrated Security=SSPI;Auto Translate=False;";Connectionstring to data warehouse database;false
    String;MIS_MTA_ConnectionString;"Data Source=.\sql2016;Initial Catalog=MIS_MTA;Provider=SQLNCLI11.1;Integrated Security=SSPI;Auto Translate=False;";Connectionstring to metadata database;false
    String;MIS_DM_ConnectionString;"Data Source=.\sql2016;Initial Catalog=MIS_DM;Provider=SQLNCLI11.1;Integrated Security=SSPI;Auto Translate=False;";Connectionstring to data mart database;false
    String;FtpPassword; 53cr3t!;Secret FTP password;true
    String;FtpUser;SSISJoost;Username for FTP;false
    String;FtpServer;ftp://SSISJoost.nl;FTP Server;false
    String;FolderStageFiles;d:\sources\;Location of stage files;false
    Boolean;EnableRetry; true;Enable retry for Webservice Task;false
    Int16;NumberOfRetries;3;Number of retries for Webservice Task;false
    Int16;SecsPauseBetweenRetry;30;Number of seconds between retry;false


    New environment

    Existing environment





























    Download scripts

    Tip: if you keep the parameter names in your SSIS projects equal to the variable names in the environment, then you could automatically references them during deployment using this PowerShell script.

    SQL Server 2016 CTP 2.3 Custom Logging Levels

    $
    0
    0
    Case
    In CTP 2.3 Custom logging levels where added. How does it work?

    Solution
    You can set it up in the Catalog with SQL Server Management Studio.

    1) Customized Logging Level
    Right click on the SSISDB Catalog and choose Customized Logging Level. A new window will be opened where you can add and change Customized Logging Levels.
    Right click the SSISDB Catalog
















    2) Create
    Click on the create button to add a name and description of your new Customized Logging Level. In this case we will create a logging level that only shows OnError events.
    Only OnError





















    3) Edit
    A new logging level has been created. You can click it to view and change the properties. Our first Customized Logging Level has ID 1.
    Edit Customized Logging Level


















    4) Statistics
    Go to the second tab to change the Statistics. In CTP 2.3 the checboxes on the individual rows seems to be missing. So I added them manually to the screenshot, but they do work when you use your Arrow keys to select the cell where they supose to be and then press [Space] to (un)check the individual item.
    
    Select Statistics options



















    5) Events
    Now go to the third tab to select the events you want to log. Same problem with the missing checkboxes here, but the same workaround works. In this case I only checked OnError events. When you're ready click on the Save button to save all changes. 
    Select the Events


















    6) Execute
    Now right click on your package to execute it and go to the Advanced tab to select the Logging Level. In the drop down list select the bottom item <Select customized logging level...>.
    Select customized logging level...

















    7) Select customized logging level
    A new popup window appears when you can select your newly created Cusomized Loging Level. Again there seems to be missing a checkbox here. By default the row seems to be already selected, so you don't have to selected it. If you do try to select it, you will uncheck it and you will get a warning:
    No customized logging level is specified. Please select a customized logging level value before performing this operation.





















    8) View details customized logging level
    When you click on the button with the three dots on the right side of the popup window, you will be able to see the details. Here the checkboxes are visible!
    View details of your customized logging level




















    9) Execution results
    Now it's time to see the result. As you can see: a customized logging level was selected and only errors are shown.


















    10) Feature request
    Besides the hidden checkboxes I also would like to make my Customized Logging Level the default for the catalog. At the moment this seems not to be possible.
    Default logging level






    SQL Server 2016 CTP 2.3 Get error columnname

    $
    0
    0
    Case
    Before SSIS 2016 there was no way to get the name of the column that caused the error. Atleast not without a custom transformation or a very complicated script that looped through a copy of your package to get all details. Now there is a simple solution available.

    Solution
    There was already a script available to get the error description. Now you can use a similar way to get the columnname. First download SSDT 2015 (SSDT and SSDT-BI have been merged!).

    1) Data Flow Task
    For this example we have a Flat File Source and to throw an error there is a column in the textfile with a too large value causing a truncation error. To catch the error details we redirect all errors of the Flat File Source to an Error Output. You can find these settings by editing the Flat File Source component or by connecting its red output to an other transformation.

    Redirect errors to Error Output

























    2) Add Script Component
    The Error Output is redirected to a Script Component (type transformation). It should look something like this below. Give it a suitable name like "SCR- Get Error Details".
    Add Script Component Transformation


















    3) Input Columns
    Edit the Script Components and go to the Input Columns page and select the ErrorCode (for getting an error description) and the ErrorColumn (for getting a column name) as ReadOnly input columns.
    Input Columns

























    4) Output Columns
    Create two output columns with the Data Type String (DT_STR). For this example I used 255 for the length, but that could probably be a little smaller. The names are ErrorDescription and ErrorColumnName.
    Output Columns

























    5) The Code
    Now go to the first page to choose your Scripting Language and then click on the Edit Script button to open the VSTA environment. Then locate the Input0_ProcessInputRow method at the bottom and add the following two lines of code.
    // C# Code
    public override void Input0_ProcessInputRow(Input0Buffer Row)
    {
    Row.ErrorDescription = this.ComponentMetaData.GetErrorDescription(Row.ErrorCode);
    Row.ErrorColumnName = this.ComponentMetaData.GetIdentificationStringByLineageID(Row.ErrorColumn);
    }

    And VB.NET code

    ' VB Code
    Public Overrides Sub Input0_ProcessInputRow(ByVal Row As Input0Buffer)
    Row.ErrorDescription = Me.ComponentMetaData.GetErrorDescription(Row.ErrorCode)
    Row.ErrorColumnName = Me.ComponentMetaData.GetIdentificationStringByLineageID(Row.ErrorColumn)
    End Sub


    6) Testing
    Now add a Data Viewer behind the Script Component Transformation to see the results
    Error Description and ColumnName

    SQL Server 2016 CTP 2.3 OData V4 protocol

    $
    0
    0
    SSIS 2016 CTP 2.3 introduces an ODdata Source Component and an OData Connection Manager with V4 support. It now supports:
    • OData V3 protocol for ATOM and JSON data formats.
    • OData V4 protocol for JSON data format.
    The 2014 version only supports V3.
    SSIS 2014: Data at the root level is invalid. Line 1, position 1. (System.Xml)

























     
    First the OData Connection Manager. You can test this with the test URL: http://services.odata.org/V4/Northwind/Northwind.svc/
    More info here.
    OData Connection Manager with V4 support

    

     




















     
     

    And there is the OData Source Component that uses the OData Connection Manager. More info here.
    OData Source




















    And now in action with a Data Viewer:
    OData with Data Viewer


    SQL Server 2016 CTP 2.3 New database roles SSISDB

    $
    0
    0
    Case
    There are two new database roles available in the SSIS Catalog (ssis_logreader and ssis_monitor). What kind of permissions do these roles have and how should we use them?
    ssis_logreader and ssis_monitor roles


























    Solution
    The database role ssis_monitor is for an AlwaysOn situation to do some clean-up and update work. The role is used by the SQL Server Agent job "SSIS Failover Monitor Job" and you shouldn't use this role yourself.

    But you can use the new database role ssis_logreader which allows a user to read reports in the catalog. If you don't have this role you can only see the reports with your own executions on it. A workaround for that was to give a user the database role ssis_admin, but allows you to play God in the catalog. This is where the new logreader role comes in handy. It allows you to see everybody's executions without the God-mode.

    SQL Saturday Holland 2015 - What's new in SSIS 2016 CTP 2.3

    Azure File System Task for SSIS

    $
    0
    0
    Case
    There is an upload and download task in the SSIS Azure Pack, but how can I delete a storage container in my Azure strorage account that was created with SSIS?

    Solution
    At the moment there is no Azure File System Task for SSIS, but you can also do this with a Script Task.

    1) Azure SDK
    First download and install the Azure SDK for .NET 2.7 (or newer). This SDK contains an assembly that we need to reference in our Script Task. When you hit the download button you can download multiple files. The one you need is called MicrosoftAzureLibsForNet-x64.msi (you can't install both 64 and 32bit).
    Libraries only is enough






















    2) SSIS Feature Pack for Microsoft Azure
    Download and install (next, next, finish) the SSIS Feature Pack for Microsoft Azure (2012, 2014).
    SSIS Azure Feature Pack






















    3 Package Parameters
    Unfortunately we cannot use the Azure Storage Connection Manager because the properties we need are sensitive (writeonly in a Script Task), therefore we will use two string package parameters. The first one contains the name of the container that you want to delete and is called "ContainerName". You can find the exact name in the Azure management portal.
    Container in Storage Account


















    The second package parameter is a sensitive string parameter named "ConnectionStringStorageAccount". It contains the connection string of the Azure Storage Account. The format should be like this (you have to replace the red parts):
    DefaultEndpointsProtocol=https;AccountName=ssisjoost;AccountKey=34PQgq+Kpr9Mz4rUfGoTpR1GZrGcC/SaFphXt3aUmgzXrcowtba0vz+uq1bIYBS5FkFYEaJ6W2CYVSsB5C8AEDQ==

    The first red part of the string is the name of the storage account. You can look it up on the Azure management portal.
    Storage Account "ssisjoost"



















    The second red part is the Account Access Key which can also be copied from Azure.
    Storage Account Access Keys



















    The end result should look like this. Of course you can use different names or project parameters instead, but then you have to change that in the Script Task!
    Package Parameters









    4) Add Script Task
    Add a Script Task to the Control Flow and give it a suitable name like "SCR - Delete Storage Container". Edit it, choose the ScriptLanguage and select the two string parameters from the previous step as ReadOnlyVariables. Then click on the Edit Script button to open the VSTA environment.
    Edit Script Task





















    5) Add reference
    In the solution explorer we first need to add a reference to one of the assemblies installed in step 1: Microsoft.Windows.Storage.dll which is located in the folder: C:\Program Files\Microsoft SDKs\Azure\.NET SDK\v2.7\ToolsRef\
    Adding a reference in C#


















    6) The code - Import custom namespaces
    To shorten the code we need to add some usings (C#) or some imports (VB). Add these just below the standard imports or usings.
    // C# Code
    #region CustomNamespaces
    using Microsoft.WindowsAzure;
    using Microsoft.WindowsAzure.Storage;
    using Microsoft.WindowsAzure.Storage.Auth;
    using Microsoft.WindowsAzure.Storage.Blob;
    #endregion

    or VB.NET code

    ' VB.NET Code
    #Region "CustomImports"
    Imports Microsoft.WindowsAzure
    Imports Microsoft.WindowsAzure.Storage
    Imports Microsoft.WindowsAzure.Storage.Auth
    Imports Microsoft.WindowsAzure.Storage.Blob
    #End Region

    7) The code Main method
    In the main method we need to replace the existing comments and code with the following code.
    // C# Code
    public void Main()
    {
    // Get parameter values. Notice the difference between
    // a normal and a sensitive parameter to get its value
    string connStr = Dts.Variables["$Package::ConnectionStringStorageAccount"].GetSensitiveValue().ToString();
    string containerName = Dts.Variables["$Package::ContainerName"].Value.ToString();

    try
    {
    // Retrieve storage account from connection string.
    CloudStorageAccount storageAccount = CloudStorageAccount.Parse(connStr);

    // Create the blob client.
    CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();

    // Create a reference to the container you want to delete
    CloudBlobContainer container = blobClient.GetContainerReference(containerName);

    // Delete the container if it exists
    container.DeleteIfExists();

    // Show success in log
    bool fireAgain = true;
    Dts.Events.FireInformation(0, "Delete Storage Container", "Container " + containerName + " was deleted successfully", string.Empty, 0, ref fireAgain);

    // Close Script Task with Success
    Dts.TaskResult = (int)ScriptResults.Success;
    }
    catch (Exception ex)
    {
    // Show Failure in log
    Dts.Events.FireError(0, "Delete Storage Container", ex.Message, string.Empty, 0);

    // Close Script Task with Failure
    Dts.TaskResult = (int)ScriptResults.Failure;
    }
    }


    or VB.NET code

    ' VB.NET Code
    Public Sub Main()
    ' Get parameter values. Notice the difference between
    ' a normal and a sensitive parameter to get its value
    Dim connStr As String = Dts.Variables("$Package::ConnectionStringStorageAccount").GetSensitiveValue().ToString()
    Dim containerName As String = Dts.Variables("$Package::ContainerName").Value.ToString()

    Try
    ' Retrieve storage account from connection string.
    Dim storageAccount As CloudStorageAccount = CloudStorageAccount.Parse(connStr)

    ' Create the blob client.
    Dim blobClient As CloudBlobClient = storageAccount.CreateCloudBlobClient()

    ' Create a reference to the container you want to delete
    Dim container As CloudBlobContainer = blobClient.GetContainerReference(containerName)

    ' Delete the container if it exists
    container.DeleteIfExists()

    ' Show success in log
    Dim fireAgain As Boolean = True
    Dts.Events.FireInformation(0, "Delete Storage Container", "Container " + containerName + " was deleted successfully", String.Empty, 0, fireAgain)

    ' Close Script Task with Success
    Dts.TaskResult = ScriptResults.Success
    Catch ex As Exception
    ' Show Failure in log
    Dts.Events.FireError(0, "Delete Storage Container", ex.Message, String.Empty, 0)

    ' Close Script Task with Failure
    Dts.TaskResult = ScriptResults.Failure
    End Try
    End Sub

    SQL Server 2016 CTP 3.0 New Control Flow Templates

    $
    0
    0

    A long long time ago (2008) there was a connect request for Package Parts to make it easier to reuse certain parts of a package. In SQL 2016 CTP 3.0 Microsoft added Control Flow Templates (probably not the best descriptive name). This blogpost describes what I have figured out so far.

    1) SSDT October preview
    First download and install the SSDT October preview for Visual Studio 2015.
    SSDT October Preview






















    2) Create new SSIS project
    Now open SSDT and create a new SSIS project. In the solution explorer you will find a new folder called Control Flow Templates. Right click it to create a new template. You can also add an existing template from an other project/folder or even reference an existing template which makes it easier to create some kind of template archive. For this example just create a new one Call it MoveToArchive.
    Control Flow Templates



























    3) Template
    When you have created a new template a new file is opened which looks just like a package. The only difference is its extension: dtsxt and it only has two tabs (control flow and data flow). For this example I added a FILE connection manager (named myFile.txt) pointing to a random file and second FILE connection manager pointing to an archive folder. A File System Task will move the file to the archive folder. Rename the task to FSYS - Move to Archive. Optionally add a annotation describing the template. Then save the template.
    File System Task moving file to archive folder






















    4)  Adding template to package
    You might have noticed the extra menu item in the SSIS Toolbar called Control Flow Templates. Drag the new template from the SSIS Toolbar to your package. You will see that the task has a T in the upper right corner and that it uses the name of the template and not the name of the task in the template. Any annotations are ignored and will not show up in the package.
    Adding template to package


















    5) Configuring the template
    Now double click the template in your package or right click it and choose Edit. In the new Template Configuration Dialog, go to the second tab called Connection Managers. Select the myFile.txt connection manager and then select the connectionstring property and change the path of it to let it point to an other file.
    Unfortunately you can only hardcode any changes. It would be useful to have expressions or even use a connection manager from your package to override the connection manager from your template.
    Template Configuration Dialog

















    6) Testing
    Now run the package to test the result.
    Running the package

















    7) Point of attentions

    • You can only have one execution in a template. If you need more tasks then you have to add them in a Sequence Container.
    • Script Tasks that uses Connection Manager wont work unless you copy the connection manager to the package. Apparently it searches the name in the package instead of in the template.
    • Renaming a template will screw up packages using that template. Make sure first give it a correct name. If you want to use naming conventions than you should give the template the name of taks. In this example: "FSYS - Move to archive.dtsxt"
    • You can't edit tasks in a template via the package. You need to do that via the template itself.


      BIML force creating connection managers

      $
      0
      0
      Case
      If you declare a connection manager in BIML, but don't use it in one of the tasks or transformations, it won't be created. Can you force BIML to create the connection managers nevertheless?


      No connection managers were created
















      Solution
      In some cases you want to override this feature and just create the connection managers. For example when using Custom Tasks/Transformations where BIML doesn't recognize a connection manager attribute.



      To force BIML to create the connection managers you need to add a second <Connections> tag, but this time within the package tag. And within this tag you can add <Connection> tags with a ConnectionName attribute. As a value you need to need to supply the name of the connection manager that you created in the first <Connections> tag.
      Force creating connection managers
















      <Biml xmlns="http://schemas.varigence.com/biml.xsd">
      <Connections>
      <AdoNetConnection ConnectionString="Data Source=.;Initial Catalog=tempdb;Integrated Security=True;"
      Provider="System.Data.SqlClient.SqlConnection, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
      Name="myStage"
      CreateInProject="true"
      />
      <FileConnection FileUsageType="ExistingFolder"
      FilePath="d:\"
      Name="myFolder"
      CreateInProject="false"
      />
      </Connections>

      <Packages>
      <Package Name="myPackage" ProtectionLevel="DontSaveSensitive">
      <Tasks>
      <Container Name="myContainer">

      </Container>
      </Tasks>
      <Connections>
      <!-- Force creating connection managers -->
      <Connection ConnectionName="myStage" />
      <Connection ConnectionName="myFolder" />
      </Connections>
      </Package>
      </Packages>
      </Biml>


      You can even determine the guid of each connection manager.
      <Connections>
      <!-- Force creating connection managers -->
      <Connection ConnectionName="myStage"
      Id="{365878DA-0DE4-4F93-825D-D8985E2765FA}"/>
      <Connection ConnectionName="myFolder"
      Id="{365878DA-0DE4-4F93-825D-D8985E2765FB}"/>
      </Connections>


      And if you need the same GUID in multiple places within your script, but you want a random GUID, then you can add a string variable and fill it with a random GUID. Then you can use that variable in multiple places.
      <Biml xmlns="http://schemas.varigence.com/biml.xsd">
      <Connections>
      <AdoNetConnection ConnectionString="Data Source=.;Initial Catalog=tempdb;Integrated Security=True;"
      Provider="System.Data.SqlClient.SqlConnection, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
      Name="myStage"
      CreateInProject="true"
      />
      <FileConnection FileUsageType="ExistingFolder"
      FilePath="d:\"
      Name="myFolder"
      CreateInProject="false"
      />
      </Connections>

      <#
      // Create Random Guid but use it in multiple places
      string myGuid = System.Guid.NewGuid().ToString();
      #>

      <Packages>
      <Package Name="myPackage" ProtectionLevel="DontSaveSensitive">
      <Tasks>
      <Container Name="myContainer">

      </Container>
      </Tasks>
      <Connections>
      <!-- Force creating connection managers -->
      <Connection ConnectionName="myStage"
      Id="<#=myGuid#>"/>
      <Connection ConnectionName="myFolder"
      Id="{365878DA-0DE4-4F93-825D-D8985E2765FB}"/>
      </Connections>
      </Package>
      </Packages>
      </Biml>

      Disable multiple checkout in TFS

      $
      0
      0
      Case
      I tried disabling the TFS multiple checkout for an SSIS project, but my collegue and me can still check out the same SSIS package at the same time causing all kinds of issues when checking in the changed package. How can I disable multiple checkout?
      Disable multiple checkout in TFS

















      Solution
      You need to check your workspaces. If one of you is still using a local workspace then you can still checkout the same package multiple times.

      Go to the Source Control Explorer in Visual Studio. You can find the link in the Team Explorer pane. Then (1) open the Workspace selectbox and choose Workspaces... In the Manage Workspaces window click (2) on the Edit... button and then (3) on the Advanced >> button.
      Edit Workspace














      In the advanced option change the location from Local to Server and click on OK and one more time in the next window.
      Set workspace location to server




















      If you both use the workspace on the server then you will get an message that the SSIS package is checked out by user xxx.
      Package checked out by colleague Menno














      Now you only need to remember to don't keep the project or solution checked out otherwise no one else can add (, delete or rename) projects/packages/connection managers/etc. When adding a new package the project will be checked out. To prevent long check outs, you should first give the new/empty package its correct name and then check-in everything (the project and the new/empty package). Only then you can start building the new package!
      Also see MSND for more information about local and server workspaces.

      Executing a PowerShell script in an SSIS package

      $
      0
      0
      Case
      I want to execute a PowerShell Script within an SSIS package, but there is no PowerShell task. How do I do that in SSIS?

      Solution
      There are 3 possible solutions within SSIS:
      1. Using a Custom Task
      2. Using the Execute Process Task
      3. Using the Script Task
      For this example I will use a simple PowerShell script that exports a list of installed hotfixes on the server to a CSV file, but the possibilities with PowerShell are endless.
      #PowerShell: c:\temp\hotfixes.ps1
      [CmdletBinding()]
      Param(
      # FilePathCsv parameter is required
      [Parameter(Mandatory=$True,Position=1)]
      [string]$FilePathCsv
      )

      # Create folder if it doesn't exists
      $FolderPath = split-path $FilePathCsv
      # Check if folder exists
      if (-Not (Test-Path $FolderPath))
      {
      Write-Host "Creating folder $FolderPath"
      New-Item -ItemType directory -Path $FolderPath
      }

      # Export list of hotfixes to CSV file
      Get-WmiObject -Class Win32_QuickFixEngineering -ComputerName .| Export-Csv $FilePathCsv

      The script has a parameter to supply a csv filepath. It checks whether the folder mentioned in this path exists. If not it creates it. Then it exports the data to specified CSV file.

      A) Using a Custom Task
      You could look for a custom task like the one on codeplex (also see this example), but it seems not to be an active project. And so far I haven't found any commercial PowerShell Tasks for SSIS.

      B) Using the Execute Process Task
      To execute the PowerShell script with an Execute Process Task you need a command that looks like:
      powershell.exe -command "c:\temp\hotfixes.ps1 -FilePathCsv c:\temp\export\hotfixes.csv"

      Add an Execute Process Task to the Control Flow and give it a suitable name like "EPR - Create hotfixes CSV". Edit it and go to the Process page. In the Excutable property you add the complete path of the PowerShell executable. For example: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
      And in the Arguments property you add the rest of the command: -command "c:\temp\hotfixes.ps1 -FilePathCsv c:\temp\export\hotfixes.csv"
      PowerShell via Execute Process Task























      This step is optional but with two SSIS string variables (or parameters) containing the paths and an expression on the arguments property you can make it a little more flexible: "-command \"" + @[User::PathPowerShellScript] + " -FilePathCsv " + @[User::PathHotfixes] + "\""
      Process Task with variables(expressions) in the arguments



















      When you run this in Visual Studio you will see a PowerShell window for a couple of seconds. With the StandardOutputVariable and StandardErrorVariable you can catch the output of the PowerShell script into an SSIS variable.
      PowerShell window when executing the Execute Process Task




















      C) Using the Script Task
      If you don't like the Execute Process Task then you could use the Script Task to execute a PowerShell script, but to use PowerShell in .NET you need to install the Windows Software Development Kit (SDK) for Windows.
       It contains the assembly System.Management.Automation.dll which we need to reference in our Script Task later on. Don't forget to install it on all your machines (DTAP: Development, Test, Acceptance and Production).
      Choose Windows Software Development Kit for the libraries





















      First add two string variables (or parameters) to your package. PathHotfixes containts the filepath of the to be created CSV file and PathPowerShellScript contains the filepath of the PowerShell file.
      String variables








      Add the Script Task to the Control Flow and give it a suitable name like "SCR - Create hotfixes CSV". Then edit it choose the Script Language (C# or VB.NET).
      Choose ScriptLanguage C# or VB




















      Then add the two string variables (or parameters) as Read Only Variables. We are using them to avoid hardcoded paths in our script.
      ReadOnlyVariables
























      After that hit the Edit Script button to open the VSTA environment. In the Solution Explorer (upper right corner) right click the References and choose Add Reference... then browse to C:\Program Files (x86)\Reference Assemblies\Microsoft\WindowsPowerShell\3.0 and select the System.Management.Automation.dll file.
      Add Reference


















      In the code we need to add extra namespaces (usings in C# and imports VB) to shorten the code. You can either add these rows to the existing namespaces by adding them in the region Namespaces. Or you can add a separate region for custom namespaces.
      #region Custom Namespaces
      using System.Management.Automation;
      using System.Management.Automation.Runspaces;
      #endregion

      or VB.NET
      #Region "Custom Namespaces"
      Imports System.Management.Automation
      Imports System.Management.Automation.Runspaces
      #End Region

      Next go to the main method and add the following code
      // C# code
      ///
      /// This method executes a PowerShell file with a parameter
      ///

      public void Main()
      {
      // Create a new Runspace to run your command in
      RunspaceConfiguration runspaceConfiguration = RunspaceConfiguration.Create();
      Runspace runspace = RunspaceFactory.CreateRunspace(runspaceConfiguration);
      runspace.Open();

      // Create a new command with a parameter
      Command command = new Command(Dts.Variables["User::PathPowerShellScript"].Value.ToString());
      CommandParameter commandParameter = new CommandParameter("FilePathCsv", Dts.Variables["User::PathHotfixes"].Value.ToString());
      command.Parameters.Add(commandParameter);

      // Execute the PowerShell script in the Runspace
      Pipeline pipeline = runspace.CreatePipeline();
      pipeline.Commands.Add(command);
      pipeline.Invoke();

      // Close the Script Task and return Success
      Dts.TaskResult = (int)ScriptResults.Success;
      }

      or VB.NET
      ' VB.NET code
      ' This method executes a PowerShell file with a parameter
      Public Sub Main()
      ' Create a new Runspace to run your command in
      Dim runspaceConfiguration As RunspaceConfiguration = RunspaceConfiguration.Create()
      Dim runspace As Runspace = RunspaceFactory.CreateRunspace(runspaceConfiguration)
      runspace.Open()

      ' Create a new command with a parameter
      Dim command As New Command(Dts.Variables("User::PathPowerShellScript").Value.ToString())
      Dim commandParameter As New CommandParameter("FilePathCsv", Dts.Variables("User::PathHotfixes").Value.ToString())
      command.Parameters.Add(commandParameter)

      ' Execute the PowerShell script in the Runspace
      Dim pipeline As Pipeline = runspace.CreatePipeline()
      pipeline.Commands.Add(command)
      pipeline.Invoke()

      ' Close the Script Task and return Success
      Dts.TaskResult = ScriptResults.Success
      End Sub

      Alternative: If you don't want to store the PowerShell code in a separate file because it makes it harder to deploy it through the DTAP environment then you can also add the PowerShell code directly in your .NET code or store it in an SSIS variable and pass that to the Script Task. In that case the code slightly changes.
      // C# code
      ///
      /// This method executes a PowerShell script
      ///

      public void Main()
      {
      // Create a new Runspace to run your script in
      RunspaceConfiguration runspaceConfiguration = RunspaceConfiguration.Create();
      Runspace runspace = RunspaceFactory.CreateRunspace(runspaceConfiguration);
      runspace.Open();

      // Create string with PowerShell code (or get it from an SSIS variable)
      string PowerShellScript = @"$FilePathCsv = ""XXX""

      # Create folder if it doesn't exists
      $FolderPath = split-path $FilePathCsv
      # Check if folder exists
      if (-Not (Test-Path $FolderPath))
      {
      Write-Host ""Creating folder $FolderPath""
      New-Item -ItemType directory -Path $FolderPath
      }

      # Export list of hotfixes to CSV file
      Get-WmiObject -Class Win32_QuickFixEngineering -ComputerName .| Export-Csv $FilePathCsv";

      // Replace the hardcode dummy path with a value from an SSIS variable
      string exportFile = Dts.Variables["User::PathHotfixes"].Value.ToString();
      PowerShellScript = PowerShellScript.Replace("XXX", exportFile);

      // Execute the PowerShell script in the Runspace
      Pipeline pipeline = runspace.CreatePipeline();
      pipeline.Commands.AddScript(PowerShellScript);
      pipeline.Invoke();

      // Close the Script Task and return Success
      Dts.TaskResult = (int)ScriptResults.Success;
      }

      or VB.NET (less readable due the lack of a good string continuation on multiple lines in VB)
      ' VB.NET code
      ' This method executes a PowerShell script
      Public Sub Main()
      ' Create a new Runspace to run your script in
      Dim runspaceConfiguration As RunspaceConfiguration = RunspaceConfiguration.Create()
      Dim runspace As Runspace = RunspaceFactory.CreateRunspace(runspaceConfiguration)
      runspace.Open()

      ' Create string with PowerShell code (or get it from an SSIS variable)
      Dim PowerShellScript As String = "$FilePathCsv = ""XXX"""& Environment.NewLine &
      "# Create folder if it doesn't exists"& Environment.NewLine &
      "$FolderPath = split-path $FilePathCsv"& Environment.NewLine &
      "# Check if folder exists"& Environment.NewLine &
      "if (-Not (Test-Path $FolderPath))"& Environment.NewLine &
      "{"& Environment.NewLine &
      " Write-Host ""Creating folder $FolderPath"""& Environment.NewLine &
      " New-Item -ItemType directory -Path $FolderPath"& Environment.NewLine &
      "}"& Environment.NewLine &
      "# Export list of hotfixes to CSV file"& Environment.NewLine &
      "Get-WmiObject -Class Win32_QuickFixEngineering -ComputerName .| Export-Csv $FilePathCsv"

      ' Replace the hardcode dummy path with a value from an SSIS variable
      Dim exportFile As String = Dts.Variables("User::PathHotfixes").Value.ToString()
      PowerShellScript = PowerShellScript.Replace("XXX", exportFile)

      ' Execute the PowerShell script in the Runspace
      Dim pipeline As Pipeline = runspace.CreatePipeline()
      pipeline.Commands.AddScript(PowerShellScript)
      pipeline.Invoke()

      ' Close the Script Task and return Success
      Dts.TaskResult = ScriptResults.Success
      End Sub


      After that close the VSTA editor and the Script Task Editor and test the Script Task. If it was successful you can use a Data Flow Task to read the CSV file. With this relative easy code and this custom task example you could create your own custom PowerShell task.


      PerformUpgrade method for custom SSIS Task is missing

      $
      0
      0
      Case
      I have an existing custom task that I want to expend with a couple of properties/features. But when I add an extra property I get an error in the LoadFromXML method in packages that where created with the old version. For a custom Component (Source/Transformation/Destination) you can use the PerformUpgrade method, but that is not available for tasks. How do I solve this?

       "Object reference not set to an instance of an object."
       occurred during "LoadFromXML" for task "myTask"















      Solution
      The LoadFromXML method gets the property values from the package XML.When it tries to get the value of your new property it fails because it doesn't exists in packages that where created with the old version of your task.

      The solution is simple. Before getting the value from the attribute you must first check if it exists and if it doesn't you can give it a default value.
      // C# code
      void IDTSComponentPersist.LoadFromXML(System.Xml.XmlElement node, IDTSInfoEvents infoEvents)
      {
      // This might occur if the task's XML has been modified outside of the Business Intelligence
      if (node.Name != "MyTask")
      {
      throw new Exception(string.Format("Unexpected task element when loading task - {0}.", "MyTask"));
      }
      else
      {
      // Populate the private property variables with values from the DTS node.
      this._hasConnectionmanagerSource = Convert.ToBoolean(node.Attributes.GetNamedItem("HasConnectionmanagerSource").Value);
      this._selectedConnectionManagerIDSource = node.Attributes.GetNamedItem("SelectedConnectionManagerIDSource").Value;
      this._selectedVariableIDSource = node.Attributes.GetNamedItem("SelectedVariableIDSource").Value;

      // Check if the new property exists in the package XML before getting it
      if (node.HasAttribute("MyNewProperty"))
      {
      // Get it if it does exist and store its value in the private property variable
      this._myNewProperty = node.Attributes.GetNamedItem("MyNewProperty").Value;
      }
      else
      {
      // Give the private property variable a default value if it doesn't exist
      this._myNewProperty = "123";
      }
      }
      }


      The new version of the task with the extra property and its default value

      Searching in packages with PowerShell

      $
      0
      0
      Case
      My client asked me to make an overview of packages that use stored procedures from a certain database. Of course I can open each package and edit each task/transformation that can use stored procedures. But they have dozens of SSIS solutions with each multiple projects and a lot of packages.

      The overview will probably be outdated before I can finish reviewing all packages. Is there a smarter, faster and less tedious solution?

      Solution
      PowerShell to the rescue! I made a small script that loops through all packages in a certain folder and then searches for certain strings in the source code of the packages. Those search strings are in an array that can be filled manually or automatically with a script that gets all stored procedures from a database.

      #PowerShell: Example 1 with hardcoded list of stored procedures
      # Where are all my packages
      $PackagePath = "H:\My Documents\Visual Studio 2013\projects\TFS"

      # Where should we save the report
      $ReportPath = "H:\My Documents\packagestoredprocedures.csv"

      # Which stored procedures do we need to find
      $StoredProcedures = @("pr_Forced_append","pr_DV_HUB","pr_DV_LINK")

      #################################################################################################
      # No need to edit below this line
      #################################################################################################

      # Declare array to store result in
      $PackageSPArray = @("Solution;Project;Package;StoredProcedure")

      # Get all SSIS packages in the configured folder and in all its subfolders
      Get-ChildItem $PackagePath -Filter “*.dtsx” -Recurse | Where-Object { $_.Attributes -ne “Directory”} |
      ForEach-Object {
      # Ignore packages in the object folder
      If (!$_.FullName.Contains("\obj\"))
      {
      # Read entire package content like it's a textfile
      $PackageXml = Get-Content $_.FullName

      # Loop through Stored Procedure array
      ForEach ($StoredProcedure in $StoredProcedures)
      {
      # Optionally write search to screen
      # Write-Host ("Looking for " + $StoredProcedure + " in package " + $_.Name)

      # Check if it contains the stored procedure
      If ($PackageXml | Select-String -Pattern $StoredProcedure)
      {
      # Optionally write find to screen
      # Write-Host ("Found " + $StoredProcedure.ToString() + " in package " + $_.Name.ToString())

      # Filling array: Solution;Project;Package;StoredProcedure
      $PackageSPArray += $_.Directory.Parent.Name + ";"+ $_.Directory.Name + ";" + $_.Name + ";" + $StoredProcedure
      }
      }
      }
      }
      # Optionally write result to screen
      # $PackageSPArray | ForEach-Object {$_}

      # Write result to file
      $PackageSPArray | % {$_} | Out-File $ReportPath


      #PowerShell: Example 2 with query to get stored procedures
      # Where are all my packages
      $PackagePath = "H:\My Documents\Visual Studio 2013\projects\TFS"

      # Which server do we connect to to find stored procedures
      $SqlServer = "MyServer\SQL2014"

      # Which database do we connect to to find stored procedures
      $Database = "DWH"

      # Where should we save the report
      $ReportPath = ("H:\My Documents\packagestoredprocedures" + $Database.ToString() + ".csv")

      #################################################################################################
      # No need to edit below this line
      #################################################################################################

      # Query database to find stored procedures in information_schema
      $StoredProceduresQry = @(Invoke-SQLCmd -query ("SELECT ROUTINE_NAME as Name from " + $Database.ToString() + ".INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_TYPE = 'PROCEDURE' and ROUTINE_NAME NOT LIKE 'sp_%' order by 1") -Server $SqlServer)

      # Store query result in array
      $StoredProcedures = @($StoredProceduresQry | select-object -ExpandProperty Name)

      # Declare array to store result in
      $PackageSPArray = @("Solution;Project;Package;StoredProcedure")

      # Get all SSIS packages in the configured folder and in all its subfolders
      Get-ChildItem $PackagePath -Filter “*.dtsx” -Recurse | Where-Object { $_.Attributes -ne “Directory”} |
      ForEach-Object {
      # Ignore packages in the object folder
      If (!$_.FullName.Contains("\obj\"))
      {
      # Read entire package content like it's a textfile
      $PackageXml = Get-Content $_.FullName

      # Loop through Stored Procedure array
      ForEach ($StoredProcedure in $StoredProcedures)
      {
      # Optionally write search to screen
      # Write-Host ("Looking for " + $StoredProcedure + " in package " + $_.Name)

      # Check if it contains the stored procedure
      If ($PackageXml | Select-String -Pattern $StoredProcedure)
      {
      # Optionally write find to screen
      # Write-Host ("Found " + $StoredProcedure.ToString() + " in package " + $_.Name.ToString())

      # Filling array: Solution;Project;Package;StoredProcedure
      $PackageSPArray += $_.Directory.Parent.Name + ";"+ $_.Directory.Name + ";" + $_.Name + ";" + $StoredProcedure
      }
      }
      }
      }
      # Optionally write result to screen
      # $PackageSPArray | ForEach-Object {$_}

      # Write result to file
      $PackageSPArray | % {$_} | Out-File $ReportPath


      There are a few considerations when using this script:
      • It's looking for pieces characters! If your stored procedure has a very general name it might find that text where it isn't used as a stored procedure.
      • It isn't honoring connection managers. If the same stored procedure is used in another database it will find it.
      • It isn't honoring schemas. If you have a stored procedure multiple times in your database, but with different schemas it can't tell the difference.
      • If your have a lot of stored procedures in your database and you have a lot of packages it could take a while, but it will always be faster then checking each package manually
      Conclusion: the script isn't very intelligent, but very useful and fast for this case. All problems above can be solved, but then the script will be much bigger and take a lot of time to develop.




      Switching Target Server Versions for custom components

      $
      0
      0
      Case
      Microsoft just released the first multi-version SSDT which can edit 2012, 2014 and 2016 SSIS packages. You can now change the Target Server Version in the properties of your SSIS project and then SSDT will convert the entire project to that SSIS version. But how can you make your custom components 'switchable'?
      Target Server Version












      Solution
      For this example I will use my own Zip Task which has a 2008, 2012, 2014 and 2016 version. There are different approaches and this is only one of them. And it doesn't descibe how to create a custom task it self, but you can read that here.

      In a Visual Studio Solution I have my 4 editions of the Zip Task. The projects and the assemblies have the same names and use the same strong name key file.
      Solution with different editions of my Zip Task





















      AssemblyInfo
      If the assemblies are identical there can only by one assembly in the GAC at a time, but we need them all in the GAC. So I gave my assemblies different versions (and a different guid). You can do that in the AssemblyInfo file of your project. See the red rectangles in the image below.
      AssemblyInfo















      GAC
      Now I can have all versions in the GAC.
      2012, 2014 and 2016 version in the GAC











      UpgradeMappings - Mappings
      Now we need to tell SSIS how to convert between 2012, 2014 and 2016. There is a subfolder in the DTS map called UpgradeMappings. Each version of SSIS has its own folders:

      SSIS 2012: C:\Program Files (x86)\Microsoft SQL Server\110\DTS\UpgradeMappings
      SSIS 2014: C:\Program Files (x86)\Microsoft SQL Server\120\DTS\UpgradeMappings
      SSIS 2016: C:\Program Files (x86)\Microsoft SQL Server\130\DTS\UpgradeMappings

      In each of these folders you need to add an XML mapping file with the name of your task (or transformation/enumerator/connection manager). For example ZipTaskMapping.xml. In this file you need to tell which assembly is the old version and which one is the new version of your task. In the SSIS 2014 UpgradeMappings folder I will use the 2012 assembly as old and the 2014 as the new assembly. The assembly strong name string can be copied from the UITypeName property in your task code (search for "UITypeName"). You can also find an example file called mapping.xml.sample in the folder which you can use to start with. Here is my 2014 example:

      <?xml version="1.0" encoding="utf-8"?>
      <Mappings xmlns="http://www.microsoft.com/SqlServer/Dts/UpgradeMapping.xsd">
      <!-- Extensions -->
      <ExtensionMapping tag="ZipTask"
      oldAssemblyStrongName="ilionx.SSIS.Tasks.Zip, ilionx.SSIS.Tasks.Zip, Version=1.12.0.0, Culture=neutral, PublicKeyToken=4b5c6d755ae87bf7"
      newAssemblyStrongName="ilionx.SSIS.Tasks.Zip, ilionx.SSIS.Tasks.Zip, Version=1.14.0.0, Culture=neutral, PublicKeyToken=4b5c6d755ae87bf7" />
      </Mappings>


      UpgradeMappings - Extensions
      In a second file we need to set an alias for our assembly. This should be done for SSIS 2014 and later only. When you use this file, SSDT will change the package XML code for your task. Instead of using the strong name string as CreationName it will use this alias. The filename should be something like ZipTaskExtensions.xml. Here is my 2014 example:
      <?xml version="1.0" encoding="utf-8" ?>
      <Extensions xmlns="http://www.microsoft.com/SqlServer/Dts/Extensions.xsd">
      <Tasks>
      <Task Identifier="ZipTask" Model=".NET">
      <CreationName>ilionx.SSIS.Tasks.Zip, ilionx.SSIS.Tasks.Zip, Version=1.14.0.0, Culture=neutral, PublicKeyToken=4b5c6d755ae87bf7</CreationName>
      </Task>
      </Tasks>
      </Extensions>

      You can download the XML example files here or download and install my ZipTask and browse to the UpgradeMappings folders.

      After you have done this for your own custom SSIS components you can safely switch between the Target Server Versions. Some extra info on upgrademappings.





      Custom SSIS Component: XML Validation Task

      $
      0
      0
      Case
      A while ago I did a post on validating XML files in SSIS with nested XSD files. The out of the box XML Task doesn't honor nested XSD files with an (include or import). The Script Task workaround is simple, but since I use it a lot I decided to make a task for it.

      Solution
      The XML Validation Task allows you to specify the XML and XSD filepaths with a Connection Manager or string variable. After that it will either succeed or fail with an error message describing what's wrong with the XML file.
      XML Validation Task V0.1

















      Please email me (address is under the Help button) bugs and feature requests for this task.
      Items to address:
      • More validation to make it really monkey proof
      • Different icon
      Thinking about:
      • Providing multiple XSD's (but how many?)
      • Providing XML and/or XSD content (not path) via string variable
      • Option to throw warning instead of error






      Disclaimer
      THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

      Downloads:
      You can download a SSIS 2008, 2012, 2014 and 2016 version on the download page.

      V0.1 Initial version for 2008 to 2016
      V0.2 ?

      Installation
      The installer registers the DLL in the GAC and copies it to the task folder of SSIS (example: C:\Program Files\Microsoft SQL Server\100\DTS\Tasks\). After that you have to restart BIDS because it caches the GAC on startup. Restarting SQL Server Data Tools is not necessary.

      Bugs or feature suggestions
      Please contact me or leave a comment if you have any suggestions or found a bug in this Custom task.

      Get packages from SSIS Catalog

      $
      0
      0
      Case
      I'm at a client and they lost the original SSIS Visual Studio projects. I have the packages deployed in the SSIS catalog. Is there a way to retrieve the packages from the catalog and create SSIS project from them?
      Integration Services Catalog













      Solution
      First you should tell your client that they need to start using TFS (or similar versioning software)! And then you could use one of these three methods to recreate the SSIS projects. The catalog contains the packages, connection managers and parameters, but not the original Visual Studio (SSDT) projects, but recreating is quite easy.

      A. Integration Service Import Project Wizard
      B. Export in SSMS
      C. PowerShell to the rescue



      A. Integration Service Import Project Wizard
      If your development PC, with SSDT/Visual Studio installed, can connect to the SQL Server with the catalog on it then this method is the easiest. Open SSDT, because it has an SSIS project with an import wizard!

      1) Create a new Integration Services project
      Instead of using the standard "Integration Services" - project, we need to use the "Integration Services Import Project Wizard" - project. For the project name you should use the name of the project in the SSIS catalog and for the solution name you could use the name of project folder from the SSIS catalog.
      Integration Services Import Project Wizard




















      2) Wizard
      Now the wizard will appear which allows you to select an SSIS Catalog project and export its content to your new SSIS project in Visual Studio. With just a few clicks your project is ready to use.
      Animated GIF of wizard


























      B. Export in SSMS
      If you cannot reach the server from within Visual Studio then there is an other option. Start SQL Server Management Studio and go to the SSIS Catalog. It has an option the export projects to an ISPAC file.

      1) Right click your project and choose Export
      When you export the project, you need to save the ISPAC file to a diskdrive or network folder. Use the project name from the catalog as the filename. 
      Add caption
















      1b) Integration Service Import Project Wizard
      If you saved the ISPAC file to a folder which can be reached from your development PC you could either go to method A and use the wizard to import the ISPAC file to your project or continue to step 2 and use an alternative method to recreate an SSIS project in SSDT.


      2) Extract ISPAC file
      If you saved the file as XXXX.ispac then you first need to rename the .ISPAC file to .ZIP, after that you can to extract all files from that zipfile.
      Unzip the ISPAC file




















      3) Create new SSIS project
      Because the solution and project files aren't saved within the Catalog you need to create a new/empty project in SSDT. For the projectname you should use the name of the project in the SSIS catalog and for the solution name you could use the name of project folder from the SSIS catalog.
      Create new SSIS project in SSDT/Visual Studio
















      4) Add items from unzipped ISPAC
      First remove the default package.dtsx file and then right click the project and choose Add, Existing Item... Then browse to your unzipped ISPAC file and select all files, except "@Project.manifest" and "[Content_Types].xml" (if you forget that the will end up in the Miscellaneous folder from your project).

      If you import the Project.params file it will notice that there is already one. Replace it and after that reload the file.











      After that you have your SSIS project back and it's probably time to put it under Source Control. Don't forget to change/check the package protection level of your packages and project.
      Add project/solution to source control












      C. PowerShell
      If you have a lot ispac files to download then you could also use PowerShell. The only thing this solution doesn't do is creating a project in Visual Studio. You could either use method A or B for that or you could even try to create the XML for the xxxx.dtproj file with PowerShell. (download example)


      #PowerShell: DownloadIspac.ps1
      ################################
      ########## PARAMETERS ##########
      ################################
      # Change Server, folder, project and download folder
      $SsisServer = "MyServer\SQL2014" # Mandatory
      $FolderName = "MyFolder" # Can be empty to download multiple projects
      $ProjectName = "MyProject" # Can be empty to download multiple projects
      $DownloadFolder = "D:\MyIspacs\" # Mandatory
      $CreateSubfolders = $true # Mandatory
      $UnzipIspac = $false # Mandatory


      #################################################
      ########## DO NOT EDIT BELOW THIS LINE ##########
      #################################################
      clear
      Write-Host "========================================================================================================================================================"
      Write-Host "== Used parameters =="
      Write-Host "========================================================================================================================================================"
      Write-Host "SSIS Server :" $SsisServer
      Write-Host "Folder Name :" $FolderName
      Write-Host "Project Name :" $ProjectName
      Write-Host "Local Download Folder :" $DownloadFolder
      Write-Host "Create Subfolders :" $CreateSubfolders
      Write-Host "Unzip ISPAC (> .NET4.5) :" $UnzipIspac
      Write-Host "========================================================================================================================================================"


      ##########################################
      ########## Mandatory parameters ##########
      ##########################################
      if ($SsisServer -eq "")
      {
      Throw [System.Exception] "SsisServer parameter is mandatory"
      }
      if ($DownloadFolder -eq "")
      {
      Throw [System.Exception] "DownloadFolder parameter is mandatory"
      }
      elseif (-not $DownloadFolder.EndsWith("\"))
      {
      # Make sure the download path ends with an slash
      # so we can concatenate an subfolder and filename
      $DownloadFolder = $DownloadFolder = "\"
      }


      ############################
      ########## SERVER ##########
      ############################
      # Load the Integration Services Assembly
      Write-Host "Connecting to server $SsisServer "
      $SsisNamespace = "Microsoft.SqlServer.Management.IntegrationServices"
      [System.Reflection.Assembly]::LoadWithPartialName($SsisNamespace) | Out-Null;

      # Create a connection to the server
      $SqlConnectionstring = "Data Source=" + $SsisServer + ";Initial Catalog=master;Integrated Security=SSPI;"
      $SqlConnection = New-Object System.Data.SqlClient.SqlConnection $SqlConnectionstring

      # Create the Integration Services object
      $IntegrationServices = New-Object $SsisNamespace".IntegrationServices" $SqlConnection

      # Check if connection succeeded
      if (-not $IntegrationServices)
      {
      Throw [System.Exception] "Failed to connect to server $SsisServer "
      }
      else
      {
      Write-Host "Connected to server" $SsisServer
      }


      #############################
      ########## CATALOG ##########
      #############################
      # Create object for SSISDB Catalog
      $Catalog = $IntegrationServices.Catalogs["SSISDB"]

      # Check if the SSISDB Catalog exists
      if (-not $Catalog)
      {
      # Catalog doesn't exists. Different name used?
      Throw [System.Exception] "SSISDB catalog doesn't exist."
      }
      else
      {
      Write-Host "Catalog SSISDB found"
      }


      ############################
      ########## FOLDER ##########
      ############################
      if ($FolderName -ne "")
      {
      # Create object to the folder
      $Folder = $Catalog.Folders[$FolderName]
      # Check if folder exists
      if (-not $Folder)
      {
      # Folder doesn't exists, so throw error.
      Write-Host "Folder" $FolderName "not found"
      Throw [System.Exception] "Aborting, folder not found"
      }
      else
      {
      Write-Host "Folder" $FolderName "found"
      }
      }


      #############################
      ########## Project ##########
      #############################
      if ($ProjectName -ne "" -and $FolderName -ne "")
      {
      $Project = $Folder.Projects[$ProjectName]
      # Check if project already exists
      if (-not $Project)
      {
      # Project doesn't exists, so throw error.
      Write-Host "Project" $ProjectName "not found"
      Throw [System.Exception] "Aborting, project not found"
      }
      else
      {
      Write-Host "Project" $ProjectName "found"
      }
      }


      ##############################
      ########## DOWNLOAD ##########
      ##############################
      Function DownloadIspac
      {
      Param($DownloadFolder, $Project, $CreateSubfolders, $UnzipIspac)
      if ($CreateSubfolders)
      {
      $DownloadFolder = ($DownloadFolder + $Project.Parent.Name)
      }

      # Create download folder if it doesn't exist
      New-Item -ItemType Directory -Path $DownloadFolder -Force > $null

      # Check if new ispac already exists
      if (Test-Path ($DownloadFolder + $Project.Name + ".ispac"))
      {
      Write-Host ("Downloading [" + $Project.Name + ".ispac" + "] to " + $DownloadFolder + " (Warning: replacing existing file)")
      }
      else
      {
      Write-Host ("Downloading [" + $Project.Name + ".ispac" + "] to " + $DownloadFolder)
      }

      # Download ispac
      $ISPAC = $Project.GetProjectBytes()
      [System.IO.File]::WriteAllBytes(($DownloadFolder + "\" + $Project.Name + ".ispac"),$ISPAC)
      if ($UnzipIspac)
      {
      # Add reference to compression namespace
      Add-Type -assembly "system.io.compression.filesystem"

      # Extract ispac file to temporary location (.NET Framework 4.5)
      Write-Host ("Unzipping [" + $Project.Name + ".ispac" + "]")

      # Delete unzip folder if it already exists
      if (Test-Path ($DownloadFolder + "\" + $Project.Name))
      {
      [System.IO.Directory]::Delete(($DownloadFolder + "\" + $Project.Name), $true)
      }

      # Unzip ispac
      [io.compression.zipfile]::ExtractToDirectory(($DownloadFolder + "\" + $Project.Name + ".ispac"), ($DownloadFolder + "\" + $Project.Name))

      # Delete ispac
      Write-Host ("Deleting [" + $Project.Name + ".ispac" + "]")
      [System.IO.File]::Delete(($DownloadFolder + "\" + $Project.Name + ".ispac"))
      }
      Write-Host ""
      }


      #############################
      ########## LOOPING ##########
      #############################
      # Counter for logging purposes
      $ProjectCount = 0

      # Finding projects to download
      if ($FolderName -ne "" -and $ProjectName -ne "")
      {
      # We have folder and project
      $ProjectCount++
      DownloadIspac $DownloadFolder $Project $CreateSubfolders $UnzipIspac
      }
      elseif ($FolderName -ne "" -and $ProjectName -eq "")
      {
      # We have folder, but no project => loop projects
      foreach ($Project in $Folder.Projects)
      {
      $ProjectCount++
      DownloadIspac $DownloadFolder $Project $CreateSubfolders $UnzipIspac
      }
      }
      elseif ($FolderName -eq "" -and $ProjectName -ne "")
      {
      # We only have a projectname, so search
      # in all folders
      foreach ($Folder in $Catalog.Folders)
      {
      foreach ($Project in $Folder.Projects)
      {
      if ($Project.Name -eq $ProjectName)
      {
      Write-Host "Project" $ProjectName "found in" $Folder.Name
      $ProjectCount++
      DownloadIspac $DownloadFolder $Project $CreateSubfolders $UnzipIspac
      }
      }
      }
      }
      else
      {
      # Download all projects in all folders
      foreach ($Folder in $Catalog.Folders)
      {
      foreach ($Project in $Folder.Projects)
      {
      $ProjectCount++
      DownloadIspac $DownloadFolder $Project $CreateSubfolders $UnzipIspac
      }
      }
      }

      ###########################
      ########## READY ##########
      ###########################
      # Kill connection to SSIS
      $IntegrationServices = $null
      Write-Host "Finished, total downloads" $ProjectCount

      Output example

      Viewing all 149 articles
      Browse latest View live