Monday 18 September 2017

Master Data Services Model Cloning

Yes! My company uses Master Data Services or MDS. Yes! It's a pain. And yes! I needed to learn how to use and administer it. For anyone that hasn't had this well meant tool forced upon on them, it feels ... (if I'm being kind) ... a little unfinished.

Based on my experience MDS administration requires an awkward mix of command line tools, the MDS web interface, and even some sql scripts and stored procs executed directly on the database. There are a few strange quirks, like the fact that individual users or groups can be set as administrators, but there is a single super-user. This super-user can only be an individual and not an AD group (although I'll be happy to be proved wrong on this!). Users in the System Administration role can deploy models, but still need to be given permissions to view and edit those models by the super-user (this has changed in SQL 2016 - multiple super-users are supported as well as AD groups are now supported as super-users).

The following article explains a certain quirk regarding deploying models across environments in order to allow for ongoing development: How to Deploy Master Data Services Models Between Environments.

The development team at my company actively maintains and develops their BI solution with MDS as a core component. I have therefore created a PowerShell script to aid in deploying and synchronising models across MDS environments. My goal was to create a script that did not require any MDS knowledge. However, it will help to have a basic understanding of how MDS works and is deployed. The script performs and supports the following:
  • Multiple models can be migrated. 
  • Multiple AD Groups and/or users can be given update permissions to the models.
  • Creates model packages (pkg files) from source MDS service and saves them to the local MDS directory specifying the -includedata switch. 
  • Copies the model packages to a central directory (or back up directory)
  • Cleans up the various directories (backup, local MDS on source and destination servers)
  • Copies the packages from the central directory (or back up directory) to the destination server
  • Deploys clones of the models to the destination server
  • Assigns Update permissions to the defined groups and/or users 
There are some restrictions / limitations:
  • If migrating multiple models they all need to use the same version. There is a parameter for the -version switch of the MDSModelDeploy createpackage command but the same value will be used for each model. If you need to deploy multiple models with different versions the script will need to be run multiple times.
  • The MDS Service name parameter value is also the same for all models being migrated. 
  • The AD Groups and users will already need to exist in MDS. If they are not there, the permissions part of the script will fail. I aim to add a check for the existence of the users and groups. 
  • I have tested this using PowerShell version 5. I have no reason to believe it wouldn't work in version 4, but I have not tested that.
  • I have only tested this script against SQL Server 2012 and 2014. 
The basic prerequisites are:
  • The script is designed to run remotely so the machine the script is run on must have access to all servers and directories.
  • The credentials under which the script is run must have all the necessary permissions on the remote servers and directories. I have tested the script as a system administrator on all servers and with full control on the file share directory used for backups.
  • The credentials under which the script is run must have at least db_owner permissions on the MDS database. I have tested this as a sys_admin on the database servers. 
  • The credentials under which the script is run must have permission to access the System Administration functional area in the target Master Data Services environment.

A downloadable version of the script is on my Google Drive.

<########################################################################################################################################################################
Created by:  Thomas Mucha
Created:     2017-09-14
Description: This script allows you to clone models with data from one MDS server to another. The CLONE funtion preserves the Models' MUID values. Cloning models allows 
             for future model updates. 
             See http://www.sqlchick.com/entries/2015/3/16/how-to-deploy-master-data-services-models-between-environments for a good explanation of MDS deployment options
             In order to run this script 
########################################################################################################################################################################>
cls
<#############################################>
<###### Modify the following parameters ######>
[string[]]$Models = ('Model1','Model2','Model3','Model4','Model5') ;
$SourceServer = "SourceServerInstance" ;
$DestinationServer = "DestinationServerInstance" ;
$MDSDatabaseName = "MDS_Database_Name" ;
$PKGBackupPath = "\\BackupDirectory\$SourceServer\MDS1"; #This directory must be accessible by both Source and Destination servers

[int]$BackupPKGCleanupPeriod = -30 ; #File age of backup PKG files in days. Any files accessed before (Get-Date).AddDays($BackupPathCleanupPeriod)} will be deleted
[int]$SourcePKGCleanupPeriod = -30 ; #File age of locally saved PKG files in days. Any files accessed before (Get-Date).AddDays($BackupPathCleanupPeriod)} will be deleted
[int]$DestinationPKGCleanupPeriod = -30 ; #File age of locally saved PKG files in days. Any files accessed before (Get-Date).AddDays($BackupPathCleanupPeriod)} will be deleted

$MDSServiceName = "MDS1" 
$MDSModelVersion = "VERSION_1"

$DeleteModelsOnDestinationServer = "No" #"Yes" to delete any existing models from the destination server with the same name as those on the source server
                                         #"No" Prints warning that models with the same name exist on both the source and the destination servers. Script exits.
[string[]]$GroupList = ("DomainName\ADGroup1","DomainName\ADGroup2"); # Groups and Users must already exist in MDS in order for the permissions to be set. 

[string[]]$UserList = ($null); #Leave NULL if no users are to be added. Only use the user list when necessary, the permissions should be handled by groups

<######## Modify the above parameters ########>
<#############################################>

[string]$date = Get-Date -format s;
$date = $date -replace "-", "";
$date = "_$date" -replace ":", "";

##Delete Models from Destination server
If ($DeleteModelsOnDestinationServer -eq "No")
{
    [String[]]$ExistingModels = $null
    foreach ($Model in $Models)
    {
        $ExistingModels = $ExistingModels + (Invoke-Sqlcmd -Query "select name from mdm.tblmodel where name = '$Model';" -ServerInstance $DestinationServer -Database $MDSDatabaseName -QueryTimeout 0).name;
    }
    If ($ExistingModels.Length -gt 1)
    {
        Write-Warning "Model(s) $ExistingModels exist on $DestinationServer. Migration has been halted!" 
        Break
    }
}
If ($DeleteModelsOnDestinationServer -eq "Yes")
{
    Write-Verbose "Deleting any existing models on $DestinationServer" -Verbose
    foreach ($Model in $Models)
    {
        Invoke-Sqlcmd -Query "declare @muid uniqueidentifier; select @muid = MUID from mdm.tblmodel where name = '$Model';
                              if @muid is not null
                              begin                         
                              exec mdm.udpModelDeleteByMUID @Model_MUID= @muid
                              end;" -ServerInstance $DestinationServer -Database $MDSDatabaseName -QueryTimeout 0;
        Write-Verbose "$Model has been found and deleted on $DestinationServer" -Verbose
    };
};

Write-Verbose "Determining MDS version and directory on destination server: $DestinationServer" -Verbose
$RemoteDestMDSPath = (Get-ChildItem -Recurse -Directory "\\$DestinationServer\c$\Program Files" | where { $_.FullName -like "*Master Data Services*Configuration" }).FullName ;
Write-Verbose "Determining MDS version and directory on source server: $SourceServer" -Verbose
$RemoteSourceMDSPath = (Get-ChildItem -Recurse -Directory "\\$SourceServer\c$\Program Files" | where { $_.FullName -like "*Master Data Services*Configuration" }).FullName ;
$LocalSourceMDSPath = $RemoteSourceMDSPath.Replace("\\$SourceServer\c$", "c:");
$LocalDestinationMDSPath = $RemoteDestMDSPath.Replace("\\$DestinationServer\c$", "c:");

##Cleanup any old Model pkg files - any "pkg" files older than 30 days will be deleted
Write-Verbose "Testing for and creating backup directory" -Verbose
if (-Not (Test-Path -Path $PKGBackupPath)) 
    { New-Item $PKGBackupPath -Type directory };
Write-Verbose "Cleaning up PKG files from backup directory" -Verbose
Get-ChildItem $PKGBackupPath | where {$_.Extension -eq '.pkg' -and $_.LastWriteTime -lt (Get-Date).AddDays($BackupPKGCleanupPeriod)} | Remove-Item ;
Write-Verbose "Cleaning up PKG files from $RemoteSourceMDSPath directory" -Verbose
Get-ChildItem $RemoteSourceMDSPath | where {$_.Extension -eq '.pkg' -and $_.LastWriteTime -lt (Get-Date).AddDays($SourcePKGCleanupPeriod)} | Remove-Item ;
Write-Verbose "Cleaning up PKG files from $RemoteDestMDSPath directory" -Verbose
Get-ChildItem $RemoteDestMDSPath | where {$_.Extension -eq '.pkg' -and $_.LastWriteTime -lt (Get-Date).AddDays($DestinationPKGCleanupPeriod)} | Remove-Item 


##Create packages from source models
Write-Verbose "Creating Model Packages on source server: $SourceServer" -Verbose
foreach ($Model in $Models)
{
    $CMD = "MDSModelDeploy createpackage -model $Model -version $MDSModelVersion  -service $MDSServiceName -package $Model$date -includedata"
    invoke-command -ComputerName $SourceServer -ArgumentList $CMD,$LocalSourceMDSPath  -ScriptBlock {
        param ($CMD,$LocalSourceMDSPath)
        Set-Location $LocalSourceMDSPath
        cmd /c $CMD
    };
};

##Move pkg files to destination server via the Backup directory
Write-Verbose "Copying Model Packages to $PKGBackupPath" -Verbose
Copy-Item "$RemoteSourceMDSPath\*$Date.pkg" "$PKGBackupPath";
Write-Verbose "Copying Model Packages to $RemoteDestMDSPath" -Verbose
Copy-Item "$PKGBackupPath\*$Date.pkg" "$RemoteDestMDSPath";

##Deploy clones of the packages to the destination server
Write-Verbose "Deploying Model Packages as clones on destination server: $DestinationServer" -Verbose
foreach ($Model in $Models)
{
    $filename = (Get-ChildItem $RemoteDestMDSPath | where {$_.Name -Like "$Model$Date.pkg"}).Name
    $CMD = "MDSModelDeploy deployclone -package ""$filename"" -service $MDSServiceName"
    invoke-command -ComputerName $DestinationServer -ArgumentList $CMD,$LocalDestinationMDSPath  -ScriptBlock {
        param ($CMD,$LocalDestinationMDSPath)
        $LocalDestinationMDSPath
        Set-Location $LocalDestinationMDSPath
        cmd /c $CMD
    };
};


##Add permissions to newly deployed models
Write-Verbose "Setting update permissions for groups and/or users" -Verbose
foreach ($Model in $Models)
{
    foreach ($Group in $GroupList)
    {
        Invoke-Sqlcmd -ServerInstance $DestinationServer -Database MDS_MasterData -Query "declare @groupid int;
                                declare @groupmuid uniqueidentifier;
                                declare @groupname nvarchar(100);
                                declare @modelid uniqueidentifier;
                                declare @modelname nvarchar(100);

                            select @groupid = ID, @groupmuid = MUID, @groupname = [Name]
                            from mdm.tblUserGroup
                            where [Name] = '$Group'

                            select @modelid = MUID, @modelname = [Name]
                            from mdm.tblModel
                            where [Name] = '$Model'

                            declare @userperms table (RoleAccess_ID int, RoleAccess_MUID uniqueidentifier, Privelege_ID int, Privelege_Name nvarchar(100), ObjectType_ID int, ObjectType_NAME nvarchar(100), Securable_ID int, Securable_MUID uniqueidentifier, Securable_Name nvarchar(100), Model_ID int, Model_MUID uniqueidentifier, Model_Name nvarchar(100), SourceUserGroup_ID int, SourceUserGroup_MUID uniqueidentifier, SourceUserGroup_Name nvarchar(100), IsModelAdministrator int)
                            insert into @userperms  
                            exec mdm.udpSecurityPrivilegesSummaryGet @SystemUser_ID=1,@Principal_ID=@groupid,@PrincipalType_ID=1,@IncludeGroupAssignments=1,@Model_ID=NULL
                            if not exists (select 1 from @userperms where ObjectType_ID = 1 and Securable_Name = @modelname and Privelege_ID = 2)

                            if not exists (select 1 from @userperms where ObjectType_ID = 1 and Securable_Name = @modelname)

                            begin

                            declare @p14 bigint
                            declare @p15 uniqueidentifier

                            exec mdm.udpSecurityPrivilegesSaveByMUID @SystemUser_ID=1
                            ,@Principal_MUID=@groupmuid
                            ,@PrincipalType_ID=2
                            ,@Principal_Name=@groupname
                            ,@RoleAccess_MUID='00000000-0000-0000-0000-000000000000'
                            ,@Object_ID=1
                            ,@Privilege_ID=2
                            ,@Model_MUID=@modelid
                            ,@Model_Name=@modelname,@Securable_MUID=@modelid,@Securable_Name=@modelname,@Status_ID=0,@Return_ID=@p14 output,@Return_MUID=@p15 output

                            end"

    }
}

if ($UserList.Length > 0)
{
    foreach ($Model in $Models)
    {
        foreach ($User in $UserList)
        {
           Invoke-Sqlcmd -ServerInstance $DestinationServer -Database MDS_MasterData -Query "declare @userid int;
                                 declare @usermuid uniqueidentifier;
                                 declare @username nvarchar(100);
                                 declare @modelid uniqueidentifier;
                                 declare @modelname nvarchar(100);

                                select @userid = ID, @usermuid = MUID, @username = UserName
                                from mdm.tbluser
                                where UserName = '$User'

                                select @modelid = MUID, @modelname = [Name]
                                from mdm.tblModel
                                where [Name] = '$Model'

                                declare @userperms table (RoleAccess_ID int, RoleAccess_MUID uniqueidentifier, Privelege_ID int, Privelege_Name nvarchar(100), ObjectType_ID int, ObjectType_NAME nvarchar(100), Securable_ID int, Securable_MUID uniqueidentifier, Securable_Name nvarchar(100), Model_ID int, Model_MUID uniqueidentifier, Model_Name nvarchar(100), SourceUserGroup_ID int, SourceUserGroup_MUID uniqueidentifier, SourceUserGroup_Name nvarchar(100), IsModelAdministrator int)
                                insert into @userperms  
                                exec mdm.udpSecurityPrivilegesSummaryGet @SystemUser_ID=1,@Principal_ID=@userid,@PrincipalType_ID=1,@IncludeGroupAssignments=1,@Model_ID=NULL
                                if not exists (select 1 from @userperms where ObjectType_ID = 1 and Securable_Name = @modelname and Privelege_ID = 2)

                                if not exists (select 1 from @userperms where ObjectType_ID = 1 and Securable_Name = @modelname)

                                begin

                                declare @p14 bigint
                                declare @p15 uniqueidentifier

                                exec mdm.udpSecurityPrivilegesSaveByMUID @SystemUser_ID=1
                                ,@Principal_MUID=@usermuid
                                ,@PrincipalType_ID=1
                                ,@Principal_Name=@username
                                ,@RoleAccess_MUID='00000000-0000-0000-0000-000000000000'
                                ,@Object_ID=1
                                ,@Privilege_ID=2
                                ,@Model_MUID=@modelid
                                ,@Model_Name=@modelname,@Securable_MUID=@modelid,@Securable_Name=@modelname,@Status_ID=0,@Return_ID=@p14 output,@Return_MUID=@p15 output

                                end"        
        }
    }
}