This is the source code that has been used to move hundreds of Dynamics CRM Organizations (Orgs) versions 8.x & 9.x from self-hosted environments to cloud servers running ‘Microsoft Dynamics 365 Customer Engagement’ – this has not been tested toward destination of ‘Microsoft Dynamics 365’ (the cloud version).
###########################################################################################################
# Automating Dynamics CRM Migration
###########################################################################################################
# Requirements:
# Credentials:
# - Server Administrator (usually a member of Domain Admins)
# - Source Application Administrator
# - Destination Application Administrator
# - Source SQL Admin (domain account recommended - not 'sa')
# - Destination SQL Admin (domain account recommended - not 'sa')
# Firewall & Services
# - WinRM reachability between Application Servers and SQL Servers
# - PowerShell Execution Policy is Unrestricted or ByPass on Migrating Nodes
###########################################################################################################
# Migration Steps:
# 0) Reverse lookup of POD number from an URL
# 1) Pick an Org
# 2) Obtain Email router password and disable the Org
# 3) Disable Source Org Deployment
# 4) Disable integration
# 5) Update Fedederation metadata
# 6) Update Local Hosts files
# 7) Ship the database to its corresponding destination
# 8) Update Host Record on Local DNS
# 9) Update Host Record on Public DNS
# 10) Perform CRM import at Destination App Server
# 11) Update Idf at destination ADFS
# 12) Recreate Email Router at destination App Server
# 13) Validation: integration, environment admin login, and domain admin login
###########################################################################################################
###############################################################################################################
# 0) Reverse lookup of POD number from Org URL
# Before running migration, it's necessary to connect to the correct environment
###############################################################################################################
# User input variables
$GLOBAL:orgName='testOrg'
$GLOBAL:sourceServers='DEV-APP01','DEV-ADFS','DEV-ROUTER01','DEV-DC01'
$GLOBAL:destinationServers='CRM-APP01','LAX-ADFS01','LAX-ROUTER01','LAX-DC04'
# This function ensures the dynamic variables are generated in the correct sequence, lest they would be invalid
function setGlobalVariables{
$GLOBAL:emailUser='[email protected]'
$GLOBAL:emailPass='PASSWORD'
$GLOBAL:sendEmailTo='[email protected]' #[email protected]
$GLOBAL:copy='[email protected]'
$GLOBAL:emailPort=587
$GLOBAL:smtp='smtp.gmail.com'
$GLOBAL:emailSubject="This task has completed: "
$GLOBAL:emailBody="This is a workflow email.<br><br>Please respond accordingly."
#sendEmail $emailUser $emailPass $sendEmailTo $copy $emailSubject $emailBody
$selectedOrg=getOrg $orgName
$GLOBAL:orgName=$selectedOrg['orgName']
$GLOBAL:friendlyName=$selectedOrg['friendlyName']
$GLOBAL:databaseName=$selectedOrg['databaseName']
$GLOBAL:orgId=$selectedOrg['orgId'] # Manual entry for the spreadsheet
$GLOBAL:sourceSqlServer=$selectedOrg['sqlServer'] # Database shipping
$GLOBAL:reportServer=$selectedOrg['srsUrl'] # CRM Import
$GLOBAL:version=$selectedOrg['version'] # Version checking
$GLOBAL:state=$selectedOrg['state']
# Set logFile
$dateStamp=[System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId( (Get-Date), 'Pacific Standard Time').tostring("MM-dd-yyyy-HHmm")+'_PST'
$GLOBAL:logFile="C:\migrationLogs\$orgName`_Migration_$dateStamp.txt"
# Define servers
$GLOBAL:sourceAppServer=$sourceServers[0]
$GLOBAL:destinationAppServer=$destinationServers[0]
$GLOBAL:credentials=@{
'kimconnect\Administrator'='PASSWORD';
'kimconnect\crmAdministrator'='PASSWORD';
'kimconnect\crm'='PASSWORD';
}
# Server Administrator Credential
$GLOBAL:serverAdminUsername='kimconnect\adminAccount'
$GLOBAL:serverAdminPassword=$credentials[$serverAdminUsername]
$GLOBAL:adminCredential=New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $serverAdminUsername,$(ConvertTo-securestring $serverAdminPassword -AsPlainText -Force)
# Update Federation Metadata at the Destination
$GLOBAL:domain='kimconnect.com'
$GLOBAL:destinationUrl="https://$orgName.$domain"
#$GLOBAL:destinationEnvironment='dev'
$GLOBAL:destinationAdfs=$destinationServers[1]
# Variables to Update Federation Metadata at the Source
#$GLOBAL:domain='kimconnect.com'
$GLOBAL:sourceUrl="https://$orgName.$domain"
$GLOBAL:sourceEnvironment=getEnvironment $sourceUrl
$GLOBAL:sourceAdfs=$sourceServers[1]
# CRM Admin credential
$GLOBAL:environment=getEnvironment $sourceUrl
$GLOBAL:crmAdmin="kimconnect\kimconnect$environment"
$GLOBAL:crmPassword=$credentials[$crmAdmin]
#$GLOBAL:crmCredential=New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $crmAdmin,$(ConvertTo-securestring $crmPassword -AsPlainText -Force)
$GLOBAL:sourceCrmAdmin="kimconnect\kimconnect$sourceEnvironment"
$GLOBAL:sourceCrmPassword=$credentials[$sourceCrmAdmin]
#$GLOBAL:sourceCrmCredential=New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $sourceCrmAdmin,$(ConvertTo-securestring $sourceCrmPassword -AsPlainText -Force)
$GLOBAL:destinationCrmAdmin="kimconnect\kimconnect$destinationEnvironment"
$GLOBAL:destinationCrmPassword=$credentials[$destinationCrmAdmin]
#$GLOBAL:destinationCrmCredential=New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $destinationCrmAdmin,$(ConvertTo-securestring $destinationCrmPassword -AsPlainText -Force)
# Variables for Integration change
$GLOBAL:integrationServer=$sourceServers[2]
$GLOBAL:integrationFile='\\DEV-MAXIMUS01\C$\Program Files\Sync360\Applications\Instances.xml'
$GLOBAL:suffixEnv='prod'
$GLOBAL:matchString="$orgName@$suffixEnv"
# Variables for host files updating function
# Special considration: Update Local Hosts Files at the Source Servers
# At this time, ADFS01 of LUME
#$orgName=''
$GLOBAL:domain='kimconnect.com'
$GLOBAL:searchString="#$sourceEnvironment deployment"
$GLOBAL:newAppServer=$destinationServers[0]
$GLOBAL:appLocalIp=(Test-Connection $newAppServer -count 1).IPV4Address.IPAddressToString;
# Variables for database shipping function
# Set database name
#$databaseName=$orgName+'_MSCRM' # should already been set prior
$GLOBAL:dbData='mscrm' # These values pertain specifically to Microsoft Dynamics CRM
$GLOBAL:dbLog='mscrm_log'
$GLOBAL:overWriteFlag=$true
$GLOBAL:sourceEnvironment=.{[void]($sourceAppServer -match '^(.+)\-');$matches[1]} # should already been set prior
#$GLOBAL:sourceSqlServer=$sourceEnvironment+'-sql01.kimconnect.com'
$GLOBAL:sourceSa="kimconnect\crmAdministrator$environment"
$GLOBAL:sourceSaPassword=$credentials[$sourceSa]
$GLOBAL:destinationEnvironment=.{[void]($destinationAppServer -match '^(.+)\-');$matches[1]}
$GLOBAL:destinationSqlServer=$destinationEnvironment+'-sql01.kimconnect.com'
$GLOBAL:destinationSa="kimconnect\crmAdministrator$destinationEnvironment"
$GLOBAL:destinationSaPassword=$credentials[$destinationSa]
$GLOBAL:destinationSaCred=New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $destinationSa,$(ConvertTo-securestring $destinationSaPassword -AsPlainText -Force)
# V8 to V9 Considerations
$GLOBAL:objectName='[dbo].[PrivilegeObjectTypeCodeView]'
$GLOBAL:objectType='view'
$GLOBAL:fromObject='[dbo].[PrivilegeObjectTypeCodes]'
# Private DNS - Active Directory
# DNS Host Record Information
# Dynamic variable
$GLOBAL:aRecord=$orgName.tolower()
$GLOBAL:zoneName='kimconnect.com'
$GLOBAL:recordName=$aRecord+'.'+$zoneName
$GLOBAL:dnsServer=$destinationServers[3]
$regexIpv4 = "\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b"
$GLOBAL:recordIP=.{$ipResult=[System.Net.Dns]::GetHostAddresses($destinationServers[0]).IPAddressToString|?{$_ -match $regexIpv4 -and $_ -notmatch "^169."}
if($ipResult.gettype() -eq [string]){return $ipResult}else{return $ipResult[0]}}
# Public DNS
$GLOBAL:publicDnsUrl='https://gator9000.hostgator.com:2083'
$GLOBAL:ipDictionary=@{
'dev-web01'='1.1.1.1';
'CRM-web01'='2.2.2.2';
}
$GLOBAL:oldPublicIp=$ipDictionary[$sourceEnvironment]
$GLOBAL:newPublicIP=$ipDictionary[$destinationEnvironment]
# CRM Import
#$GLOBAL:reportServer="http://$destinationEnvironment-async01/ReportServer"
# Validation
$GLOBAL:testUsers=@("kimconnect\crmAdminstrator$destinationEnvironment",'kimconnect\crmAdminstrator','kimconnect\crmAdminstrator')
# Email Router
$GLOBAL:emailRouterUrl="https://dev-$destinationEnvironment.$domain/$orgName"
# Data Encryption
$GLOBAL:dataEncryptionUrl="https://$orgName.$domain/tools/sqlencryption/sqlencryption.aspx"
#$GLOBAL:crmAdmin='kimconnect\crmAdminstrator'
#$GLOBAL:crmAdminPassword=$credentials[$crmAdministrator]
# Password Safe
$GLOBAL:secRepoUrl='https://passwordsafe.kimconnect.com/'
# SSRS Custom Reports
$GLOBAL:sourceReportingUrl="http://$sourceEnvironment-async01/Reports/browse/$orgName`_MSCRM"
$GLOBAL:destinationReportingUrl="http://$destinationEnvironment-async01/Reports/browse/$orgName`_MSCRM"
$GLOBAL:sourceReportLoginId="kimconnect\crmAdminstrator$sourceEnvironment"
$GLOBAL:sourceReportPassword=$credentials[$sourceReportLoginId]
$GLOBAL:destinationReportLoginId="kimconnect\crmAdminstrator$destinationEnvironment"
$GLOBAL:destinationReportPassword=$credentials[$destinationReportLoginId]
# Documentation
$GLOBAL:documentationUrl='https://wiki.kimconnect.com/'
# PowerShell version
$GLOBAL:isPowerShellVersion5=$PSVersionTable.PSVersion.Major -ge 5
}
# To determine which host to initiate the migration, it's adviseable to run this function prior from a workstation
#$targetUrl='test.kimconnect.com'
function getEnvironment($url){
$regexDomain='(http[s]:\/{2}){0,1}([\d\w\.-]+)\/{0,1}'
$innerUrl=.{[void]($url -match $regexDomain);$matches[2]}
#$publicIP=[System.Net.Dns]::GetHostAddresses($domain)[0].IPAddressToString
$publicIP=(Resolve-DnsName -Name $innerUrl -Server '8.8.8.8' -NoHostsFile)[0].IPAddress
$ipDictionary=@{
'DEV-APP01'='1.1.1.1';
'CRM-APP01'='2.2.2.2';
}
$environment=$ipDictionary.keys | Where-Object {$ipDictionary["$_"] -eq $publicIP}
if ($environment){
#write-host "Environment is: $environment";
return $environment
}
else{write-warning "No environments were matched.";return $false}
}
#getEnvironment $targetUrl
###############################################################################################################
# This function sets these variables: $orgName, $friendlyName, $databaseName, and $logFile
# This function can be skipped if variables are to be set manually
function appendContent($file,$text){
if(!(test-path $file)){
$folder=Split-Path $file -Parent
$name=Split-Path $file -Leaf
New-Item -itemType File -Path $folder -Name $name -Force
}
Add-Content -path $file -value $text -PassThru
}
function selectOrg {
function pickList($list){
# Although it's more efficient to obtain the index and set it as display,
# humans prefer see a list that starts with 1, instead of 0
$display=for ($i=0;$i -lt $list.count;$i++){
"$($i+1)`:`t$($list[$i])`r`n";
}
$lines=($display | Measure-Object -Line).Lines
write-host $($display)
$maxAttempts=3
$attempts=0;
while ($attempts -le $maxAttempts){
if($attempts++ -ge $maxAttempts){
write-host "Attempt number $maxAttempts of $maxAttempts. Exiting loop..`r`n"
break;
}
$userInput = Read-Host -Prompt 'Please pick a number from the list above';
try {
$value=[int]$userInput;
}catch{
$value=-1;
}
if ($value -lt 1 -OR $value -gt $lines){
cls;
write-host "Attempt number $attempts of $maxAttempts`: $userInput is an invalid value. Try again..`r`n"
write-host $display
}else{
$item=$list[$value-1];
write-host "$userInput corresponds to $item`r`n";
return $item
}
}
}
# Set Org name and database name - run this prior to pasting the rest of the lines below
if(!(get-command get-crmorganization -ea SilentlyContinue)){Add-PSSnapin Microsoft.Crm.PowerShell}
$orgs=Get-CrmOrganization|?{$_.State -ne 'Disabled'}
try{$orgsValue=$orgs.gettype().Name}
catch{write-warning "No Orgs detected on $env:computername. Exiting Program"; break;}
if ($orgsValue -ne 'Organization'){
$orgNames=$orgs.UniqueName|sort
$orgName=$(pickList $orgNames)
}
else{$orgName=$orgs.UniqueName}
$pickedOrg=$orgs|?{$_.UniqueName -eq $orgName}
return @{
'orgName'=$pickedOrg.UniqueName
'friendlyName'=$pickedOrg.FriendlyName
'sqlServer'=$pickedOrg.SqlServerName
'databaseName'=$pickedOrg.DatabaseName
'srsUrl'=$pickedOrg.SrsUrl
'orgId'=$pickedOrg=$org.Id
'version'=$pickedOrg.Version
'state'=$pickedOrg.State
}
}
function getOrg ($orgName){
if(!(get-command get-crmorganization -ea SilentlyContinue)){Add-PSSnapin Microsoft.Crm.PowerShell}
$org=Get-CrmOrganization|?{$_.UniqueName -eq $orgName}
if(!$org){
write-warning "$orgName not found. Please try selecting from this list."
return selectOrg
}else{
return @{
'orgName'=$org.UniqueName
'friendlyName'=$org.FriendlyName
'sqlServer'=$org.SqlServerName
'databaseName'=$org.DatabaseName
'srsUrl'=$org.SrsUrl
'orgId'=$orgId=$org.Id
'version'=$org.Version
'state'=$org.State
}
}
}
# 1) Obtain Email router password and disable the Org
function sendKeysToProgram($programExe,$programTitle,$sendkeys,$waitSeconds){
$processes=get-process
$processStarted=$programExe -in $processes.Path
if($processStarted){
$processMatch=$processes|?{$programExe -eq $_.Path}
stop-process $processMatch -Force
}
start-process $programExe|out-null
$wshell = New-Object -ComObject wscript.shell;
$wshell.AppActivate($programTitle)|out-null
sleep $waitSeconds
$wshell.SendKeys($sendkeys)
}
function getEmailRouter($orgName){
# Set CRM bin folder according to its discovered location on this host
$possibleCrmPaths=@(
'C:\Program Files\Microsoft Dynamics CRM\CRMWeb\bin',
'C:\Program Files\Dynamics 365\CRMWeb\bin',
'D:\Program Files\Microsoft Dynamics CRM\CRMWeb\bin',
'E:\Program Files\Microsoft Dynamics CRM\CRMWeb\bin'
)
$possibleCrmEncryptionPaths=@(
'C:\Program Files\Microsoft CRM Email\Service\EncryptionKey.xml',
'D:\Program Files\Microsoft CRM Email\Service\EncryptionKey.xml',
'E:\Program Files\Microsoft CRM Email\Service\EncryptionKey.xml'
)
$possibleEmailAgentPaths=@(
'C:\Program Files\Microsoft CRM Email\Service\Microsoft.Crm.Tools.EmailAgent.xml',
'D:\Program Files\Microsoft CRM Email\Service\Microsoft.Crm.Tools.EmailAgent.xml',
'E:\Program Files\Microsoft CRM Email\Service\Microsoft.Crm.Tools.EmailAgent.xml'
)
$crmPath=.{$possibleCrmPaths|%{if(Test-Path $_){return $_}}}
$crmEncryptionXml=.{$possibleCrmEncryptionPaths|%{if(Test-Path $_){return $_}}}
$emailAgentFile=.{$possibleEmailAgentPaths|%{if(Test-Path $_){return $_}}}
if($crmPath){
Add-Type -Path "$crmPath\Microsoft.Xrm.Service.dll"
Add-Type -Path "$crmPath\Microsoft.Crm.dll"
$encryption = New-Object -TypeName Microsoft.Crm.Encryptor -ArgumentList $crmEncryptionXml
[xml]$emailRouterXml = Get-Content $emailAgentFile
$emailRouterConfig = $emailRouterXml.Configuration.ProviderConfiguration|?{$_.CrmServerUrl -match "/$orgName$"}
if($emailRouterConfig){
if($emailRouterConfig.count -gt 1){write-warning "There are $($emailRouterConfig.count) records"}
$result=$emailRouterConfig|Select EmailAuthMode,EmailUser, `
@{Name="emailPassword";e={$encryption.Decrypt($_.EmailPassword)}}, `
@{Name="smtpServer";e={$_.Target}}, `
@{Name="port";e={$_.EmailPort}}, `
@{Name="useSsl";e={$_.EmailUseSSL}}
return $result
}
else{
write-host "No email router records found for $orgName"
return $null
}
}else{
Write-Host "CRM path was not found."
return $false
}
}
function sendEmail{
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$emailFrom,
[Parameter(Mandatory)][string]$emailPassword,
[Parameter(Mandatory)][string[]]$emailTo,
[Parameter(Mandatory=$false)][string[]]$cc,
[Parameter(Mandatory=$false)]$subject="Test Email to Validate SMTP",
[Parameter(Mandatory=$false)]$body="This is a test email.<br><br>Please disregard",
[Parameter(Mandatory=$false)]$smtpServer=$null,
[Parameter(Mandatory=$false)]$port=587,
[Parameter(Mandatory=$false)]$attachments,
[Parameter(Mandatory=$false)]$useSsl=$true,
[Parameter(Mandatory=$false)]$anonymous=$false
)
$commonSmtpPorts=@(25,587,465,2525)
function Check-NetConnection($server,$port,$timeout=100,$verbose=$false) {
$tcp = New-Object System.Net.Sockets.TcpClient;
try {
$connect=$tcp.BeginConnect($server,$port,$null,$null)
$wait = $connect.AsyncWaitHandle.WaitOne($timeout,$false)
if(!$wait){
$tcp.Close()
if($verbose){
Write-Host "Connection Timeout" -ForegroundColor Red
}
Return $false
}else{
$error.Clear()
$null=$tcp.EndConnect($connect) # Dispose of the connection to release memory
if(!$?){
if($verbose){
write-host $error[0].Exception.Message -ForegroundColor Red
}
$tcp.Close()
return $false
}
$tcp.Close()
Return $true
}
} catch {
return $false
}
}
function getMxRecord($emailAddress){
$regexDomain="\@(.*)$"
$domain=.{[void]($emailAddress -match $regexDomain);$matches[1]}
$mxDomain=(resolve-dnsname $domain -type mx|sort -Property Preference|select -First 1).NameExchange
$detectedSmtp=switch -Wildcard ($mxDomain){ # need to build up this list
"*outlook.com" {"smtp.office365.com";break}
"*google.com" {"smtp.gmail.com";break}
"*yahoodns.net" {'smtp.mail.yahoo.com';break}
"*inbox.com" {'my.inbox.com;break'}
"*mail.com" {'smtp.mail.com';break}
"*icloud.com" {'smtp.mail.me.com';break}
"*zoho.com" {'smtp.zoho.com';break}
default {$mxDomain}
}
if($mxDomain){
write-host "Detected MX Record`t: $mxDomain`r`nKnown SMTP Server`t: $detectedSmtp"
return $detectedSmtp
}
else{
write-warning "MX record not available for $emailAddress"
return $null
}
}
if($emailFrom -match '@' -and $emailPassword){
$encryptedPass=ConvertTo-SecureString -String $emailPassword -AsPlainText -Force
$emailCred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $emailFrom,$encryptedPass
}elseif($anonymous){
$nullPassword = ConvertTo-SecureString 'null' -asplaintext -force
$emailCred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList 'NT AUTHORITY\ANONYMOUS LOGON', $pass
}elseif($emailPassword){
$emailCred=New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $emailFrom,$emailPassword
[string]$emailFrom=$emailTo|select -First 1
}else{
$emailCred=$false
}
$detectedSmtpServer=if($emailFrom -match '@' -and !$anonymous){getMxRecord $emailFrom}else{$null}
$smtpServer=if($smtpServer){
if($smtpServer -eq $detectedSmtpServer){
Write-host "Detected SMTP server matches the provided value: $smtpServer"
}else{
write-warning "Detected SMTP server $detectedSmtpServer does not match given values. Program will use the provided value: $smtpServer"
}
$smtpServer
}else{
write-host "Using detected SMTP server $detectedSmtpServer"
$detectedSmtpServer
}
$secureSmtpParams = @{
From = $emailFrom
To = $emailTo
Subject = $subject
Body = $body
BodyAsHtml = $true
DeliveryNotificationOption = 'OnFailure','OnSuccess'
Port = $port
UseSSL = $useSsl
}
$relaySmtpParams=@{
From = $emailFrom
To = $emailTo
Subject = $subject
Body = $body
BodyAsHtml = $true
DeliveryNotificationOption = 'OnFailure', 'OnSuccess'
Port = 25
UseSSL = $useSsl
}
if ($port -ne 25){
write-host "Secure SMTP Parameters detected."
$emailParams=$secureSmtpParams
}else{
write-host "Unsecured SMTP Parameters detected."
$emailParams=$relaySmtpParams
}
write-host "$($emailParams|out-string)"
try{
$sendmailCommand="Send-MailMessage `@emailParams -SmtpServer $smtpServer $(if($cc){"-cc $cc"}) $(if($emailCred){"-Credential `$emailCred"}) $(if($attachments){"-Attachments `$attachments"}) -ErrorAction Stop"
write-host $sendmailCommand
Invoke-Expression $sendmailCommand
write-host "Email has been sent to $emailTo successfully"
return $true;
}
catch{
#$errorMessage = $_.Exception.Message
#$failedItem = $_.Exception.ItemName
#write-host "$errorMessage`r`n$failedItem" -ForegroundColor Yellow
Write-Warning "Initial attempt failed!`r`n$_`r`nNow scanning open ports..."
$openPorts=$commonSmtpPorts|?{Check-NetConnection $smtpServer $_}
write-host "$smtpServer has these SMTP ports opened: $(if($openPorts){$openPorts}else{'None'})"
if($detectedSmtpServer -ne $smtpServer){
try{
write-host "Program now attempts to use the detected SMTP Server: $detectedSmtpServer"
Invoke-Expression "Send-MailMessage `@emailParams -SmtpServer $detectedSmtpServer $(if($attachments){"-Attachments $attachments"}) -ErrorAction Stop"
write-host "Email has been sent to $emailTo successfully via alternative SMTP Server: $detectedSmtpServer" -ForegroundColor Green
return $true;
}catch{
write-host $error[0].Exception.Message -ForegroundColor Yellow
return $false
}
}else{
return $false
}
}
}
# 2) Disable Source Org Deployment
function disableOrg($appServer,$orgName,$adminCredential,$logFile){
#$session=new-pssession $appServer -Credential $adminCredential
$job=Start-Job -ScriptBlock {
param($orgName)
write-host "running as: $env:username"
try{
Add-PSSnapin Microsoft.Crm.PowerShell
Disable-CrmOrganization -Name $orgname -ea Stop
return $true
}
catch{
write-warning "$error"
return $false
}
} -Credential $adminCredential -Args $orgName
Wait-Job $job|out-null
$jobResult=Receive-Job $job
#$disabled=Invoke-Command -ComputerName $appServer -Credential $adminCredential -ScriptBlock{
# param($orgName)
# try{
# Disable-CrmOrganization -Name $orgname -ea Stop
# return $true
# }
# catch{
# write-warning "$error"
# return $false
# }
# } -Args $orgName
if(!$disabled){
write-warning "$orgName cannot be disabled via PowerShell; hence, one must perform this step via GUI"
pause
$programExe="C:\Program Files\Dynamics 365\Tools\Microsoft.Crm.DeploymentManager.exe"
$programTitle='Dynamics 365 Deployment Manager'
$sendKeys='{DOWN}{DOWN}{TAB}'
sendKeysToProgram $programExe $programTitle $sendKeys 5
}
appendContent $logFile "$(get-date)`t: $orgname disabled"
#Remove-PSSession $session
# Invoke-command it to work around this error
#Disable-CrmOrganization : Source : mscorlib
#Method : HandleReturnMessage
#Date : 8:57:28 PM
#Time : 6/12/2020
#Error : Message: The Deployment Service cannot process the request because one or more validation checks failed.
#ErrorCode: -2147167645
#Stack Trace :
#======================================================================================================================
#Inner Exception Level 1 :
#==DeploymentServiceFault Info==========================================================================================
#Error : The Deployment Service cannot process the request because one or more validation checks failed.
#Time : 6/13/2020 12:57:28 AM
#ErrorCode : -2147167645
#Date : 8:57:28 PM
#Time : 6/12/2020
#Error Items:
# ActiveDirectoryRightsCheck raising error : The current user does not have required permissions (read/write) for
#the
#following Active Directory group: CN=ReportingGroup ,OU=CRM Security
#Groups,DC=intranet,DC=domain,DC=com
# SysAdminCheck raising error : You do not have sufficient permission to perform this operation on the specified
#organization database
#======================================================================================================================
#At line:1 char:1
#+ Disable-CrmOrganization -Name 'Something'
#+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# + CategoryInfo : InvalidData: (Microsoft.Crm.P...anizationCmdlet:DisableCrmOrganizationCmdlet) [Disable-C
# rmOrganization], FaultException`1
# + FullyQualifiedErrorId : CRM Deployment Cmdlet Error,Microsoft.Crm.PowerShell.DisableCrmOrganizationCmdlet
}
#disableOrg $sourceAppServer $orgName $adminCredential $logFile
# 3) Disable integration
function disableIntegration($integrationServer,$adminCredential,$integrationFile,$matchString,$logFile){
$disableIntegration=invoke-command -ComputerName $integrationServer -Credential $adminCredential {
param($file,$match)
$content=get-content $file
$matchLine=$content -match $match
if($matchLine){
$replace=($matchLine -replace '\<add\>','<!--add>') +' -->'
if($replace){
$updatedContent=$content -replace $matchLine,$replace
Rename-Item -Path $file -NewName "$(split-path $file -leaf)`.bak"
$updatedContent|set-content -path $file -Force
write-host "$updatedContent has been updated successfully."
return $true
}else{
write-host "No update required for $matchLine"
return $false
}
}else{
write-host "no matches found with keyword: $match"
return $false
}
} -args $integrationFile,$matchString
appendContent $logFile "$(get-date)`t: integration disabled = $disableIntegration"
}
#disableIntegration $integrationServer $adminCredential $integrationFile $matchString $logFile
function enableIntegration($integrationServer,$adminCredential,$integrationFile,$matchString,$logFile){
$enableIntegration=invoke-command -ComputerName $integrationServer -Credential $adminCredential {
param($file,$match)
$content=get-content $file
$matchLine=$content -match $match
if($matchLine){
$replace=$matchLine -replace '!--',''
if($replace){
$updatedContent=$content -replace $matchLine,$replace
Rename-Item -Path $file -NewName "$(split-path $file -leaf)`.bak"
$updatedContent|set-content -path $file -Force
write-host "$updatedContent has been updated successfully."
return $true
}else{
write-host "No update required for $matchLine"
return $false
}
}else{
write-host "no matches found with keyword: $match"
return $false
}
} -args $integrationFile,$matchString
appendContent $logFile "$(get-date)`t: integration re-enabled = $enableIntegration"
}
# 4) Update Fedederation metadata
function invokeIfdUpdate($adfs,$cred,$environment,$url){
function updateIfdRelyingPartyTrust($environment,$url){
$targetName="$environment IFD Relying Party"
$availableNames=(Get-AdfsRelyingPartyTrust).Name
$match=$availableNames|?{$_ -like "$targetName*"}
if ($match){
Update-ADFSRelyingPartyTrust -TargetName "$match"
$urlExists=Get-AdfsRelyingPartyTrust -Identifier "$url"
if($urlExists){
return "$url currently exists on ADFS server $env:computername as '$($targetName.ToUpper())'"
}
else{return "$url currently NOT exists on $targetName"}
}else{
return "$targetname not found."
}
}
invoke-command -ComputerName $adfs -Credential $cred -ScriptBlock{
param($importedFunc,$x,$y)
write-host "Executing function on $env:computername"
return [ScriptBlock]::Create($importedFunc).invoke($x,$y);
} -args ${function:updateIfdRelyingPartyTrust},$environment,$url
}
function updateFederationMetadata{
param($adfs,$adminCredential,$environment,$url)
$sourceIfdResult=invokeIfdUpdate $adfs $adminCredential $environment $url
return $sourceIfdResult
}
function confirmation($content,$testValue="I confirm",$maxAttempts=3){
$confirmed=$false;
$attempts=0;
$content|write-host
write-host "Please review this content for accuracy.`r`n"
while ($attempts -le $maxAttempts){
if($attempts++ -ge $maxAttempts){
write-host "A maximum number of attempts have reached. No confirmations received!`r`n"
break;
}
$userInput = Read-Host -Prompt "Please type in this value => $testValue <= to confirm. Input CANCEL to skip this item";
if ($userInput.ToLower() -eq $testValue.ToLower()){
$confirmed=$true;
write-host "Confirmed!`r`n";
break;
}elseif($userInput -like 'cancel'){
write-host 'Cancel command received.'
$confirmed=$false
break
}else{
cls;
$content|write-host
write-host "Attempt number $attempts of $maxAttempts`: $userInput does not match $testValue. Try again or Input CANCEL to skip this item`r`n"
}
}
return $confirmed;
}
function updateHostFiles{
param(
$appLocalIp, $orgName, $domain, $searchString, $servers,$commentOut=$false
)
# placing this supporting function for easy copy/paste
function confirmation($content,$testValue="I confirm",$maxAttempts=3){
$confirmed=$false;
$attempts=0;
$content|write-host
write-host "Please review this content for accuracy.`r`n"
while ($attempts -le $maxAttempts){
if($attempts++ -ge $maxAttempts){
write-host "A maximum number of attempts have reached. No confirmations received!`r`n"
break;
}
$userInput = Read-Host -Prompt "Please type in this value => $testValue <= to confirm";
if ($userInput.ToLower() -ne $testValue.ToLower()){
cls;
$content|write-host
write-host "Attempt number $attempts of $maxAttempts`: $userInput does not match $testValue. Try again..`r`n"
}else{
$confirmed=$true;
write-host "Confirmed!`r`n";
break;
}
}
return $confirmed;
}
function generateARecord{
param(
$newARecord,
$domain,
$ip
)
$url=$newARecord+'.'+$domain;
$recordString="$ip`t$url";
return $recordString;
}
function updateHostRecord{
param(
$record,
$insertAfter,
$servers,
$remove=$false
)
$hostFiles=$($servers|%{"\\$_\C$\windows\system32\drivers\etc\hosts"})
$results=@{}
#replaceLine -textContent $fileContent -match $url -updateLineContent $record -remove $remove
function replaceLine($textContent,$match,$updateLineContent,$remove=$false){
write-host "Search string: $match"
$lineMatch = $textContent | Select-String "$match"
$lineNumber = $lineMatch.LineNumber
$lineContent = $lineMatch.Line
$replaceWith=if($remove){if($lineContent[0] -eq '#'){$lineContent}else{"#$lineContent #record disabled $(get-date) by $(whoami)"}}
else{$updateLineContent}
write-host "Replacing`t: $lineMatch`r`nWith`t: $replaceWith"
$textContent[$lineNumber-1]=$replaceWith
return $textContent
}
function updateLocalHostRecord{
param(
$record,
$hostfile="c:\windows\system32\drivers\etc\hosts",
$searchString=$false,
$remove=$false
)
function insertAfterMatchString{
param(
$content,
$searchString,
$insert,
$remove=$false
)
if($remove){$insert='#'+$insert}
[int]$matchedIndex=.{$lines=[array]$content
for ($i=0;$i -lt $lines.Count;$i++) {if($lines[$i] -like "$searchString*"){return $i}}
}
if($matchedIndex){
$nextAvailableSpaceIndex=.{for($i=$matchedIndex;$i -lt $content.Length;$i++){
$isEmpty=$content[$i] -match "^\s*$"
#$isEmpty=$content[$i].Trim().isEmpty()
if ($isEmpty){return $i}
}
}
$content=$content[0..$($nextAvailableSpaceIndex-1)]+$insert+$content[$($nextAvailableSpaceIndex)..$($content.length-1)]
write-host "$insert will be inserted after $($content[$nextAvailableSpaceIndex-1])`r`n-----------------------`r`n"
}else{
$content+=$insert
write-host "$searchString has not matched anything.";
}
return $content
}
$validPath=[System.IO.File]::Exists($hostfile) # Slightly faster than Test-Path for true positives but not true negatives
if($validPath){
$fileContent=Get-Content -Path $hostfile
$url=.{[void]($record -match "([0-9A-Za-z_\.\-]*)$");$matches[1]}
$entryExists=$fileContent|?{$_ -match "$url"}|select -First 1
write-host "Entry exists: $entryExists"
if(!$entryExists){
write-host "Search string: '$searchString'"
pause
if($searchString){
$fileContent=insertAfterMatchString -content $fileContent -searchString $searchString -insert $record $remove
}else{
$fileContent+=$(if($remove){'#'})+$record
}
$confirmed=confirmation -content $fileContent
if ($confirmed){
$backupFile=$hostFile+"_backup"
try{
Rename-Item -Path $hostfile -NewName $backupFile -ErrorAction Stop
}
catch{
write-host "Unable to rename. Now trying to delete previous backup and retry"
Remove-item $backupFile -Force
Rename-Item -Path $hostfile -NewName $backupFile
}
# The try-catch method overcomes this error
#Rename-Item : Cannot create a file when that file already exists.
#At line:1 char:1
#+ Rename-Item -Path $hostfile -NewName $($hostFile+"_backup") -Force
#+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# + CategoryInfo : WriteError: (C:\windows\system32\drivers\etc\hosts:String) [Rename-Item], IOException
# + FullyQualifiedErrorId : RenameItemIOError,Microsoft.PowerShell.Commands.RenameItemCommand
$fileContent|set-Content $hostfile
$message="$hostfile updated successfully."
write-host $message -ForegroundColor Green
return $message
}else{
$message="$hostfile update cancelled."
write-host $message -ForegroundColor Red
return $message;
}
}
else{
$fileContent=replaceLine -textContent $fileContent -match $url -updateLineContent $record -remove $remove
$confirmed=confirmation -content $fileContent
if ($confirmed){
$backupFile=$hostFile+"_backup"
try{
Rename-Item -Path $hostfile -NewName $backupFile -ErrorAction Stop
}
catch{
write-host "Unable to rename. Now trying to delete previous backup and retry"
Remove-item $backupFile -Force
Rename-Item -Path $hostfile -NewName $backupFile
}
# The try-catch method overcomes this error
#Rename-Item : Cannot create a file when that file already exists.
#At line:1 char:1
#+ Rename-Item -Path $hostfile -NewName $($hostFile+"_backup") -Force
#+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# + CategoryInfo : WriteError: (C:\windows\system32\drivers\etc\hosts:String) [Rename-Item], IOException
# + FullyQualifiedErrorId : RenameItemIOError,Microsoft.PowerShell.Commands.RenameItemCommand
$fileContent|set-Content $hostfile
$message="$hostfile updated successfully."
write-host $message -ForegroundColor Green
return $message
}else{
$message="$hostfile update cancelled."
write-host $message -ForegroundColor Red
return $message;
}
}
}else{
$message="Unable to access $hostfile. Action aborted.";
write-warning $message
return $message;
}
}
for ($i=0;$i -lt $hostFiles.count; $i++){
$hostfile=$hostFiles[$i]
write-host "Processing $hostfile..."
$result=updateLocalHostRecord -record $record -hostfile $hostFile -searchString $insertAfter -remove $remove
$results.Add($servers[$i],$result)
}
return $results
}
function updateHostRecordOnServers{
param(
$appLocalIp,
$orgName,
$domain,
$searchString,
$servers,
$remove=$false
)
$regexIP = [regex] "\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b"
if ($appLocalIp -match $regexIP){
$newRecord=generateARecord -newARecord $orgName -domain $domain -ip $appLocalIp
$result=updateHostRecord -record $newRecord -insertAfter $searchString -servers $servers -remove $remove
}else{
$result="Unable to retrieve the app server private IP. Cannot update host records without that information."
write-warning $result
}
return $result
}
return updateHostRecordOnServers $appLocalIp $orgName $domain $searchString $servers $commentOut
#$regexIP = [regex] "\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b"
#$newRecord=generateARecord -newARecord $orgName -domain $domain -ip $appLocalIp
#foreach ($server in $servers){
# if ($appLocalIp -match $regexIP){
# #updateHostRecord -record $newRecord -insertAfter $searchString -servers $server -remove $remove
# invoke-command -ComputerName $server -credential $adminCredential -scriptblock {
# param ($updateHostRecord,$newRecord, $searchString, $server, $remove)
# [ScriptBlock]::Create($updateHostRecord).invoke($newRecord, $searchString, $server, $remove)
# } -Args ${function:updateHostRecord},$newRecord, $searchString, $server, $remove
# }
# else{
# write-host "Unable to retrieve the app server private IP. Cannot update host records without that information."
# }
#}
}
# updateHostFiles $appLocalIp $orgName $domain $searchString $sourceServers $commentOut
function invokeUpdateHostFiles{
param(
$adminCredential,$serverLocalIp,$orgName, $domain, $searchString,$targetHosts,$remove=$false
)
$jobResults=start-job -credential $adminCredential -scriptblock {
param ($updateHostFiles,$serverLocalIp, $orgName, $domain, $searchString, $targetHosts,$remove)
[ScriptBlock]::Create($updateHostFiles).invoke($serverLocalIp, $orgName, $domain, $searchString, $targetHosts,$remove)
} -Args ${function:updateHostFiles},$serverLocalIp, $orgName, $domain, $searchString, $targetHosts,$remove|Receive-Job -Wait
return $jobResults
}
#invokeUpdateHostFiles $adminCredential $appLocalIp $orgName $domain $searchString $sourceServers $true
# 7) Ship the database to its corresponding destination
# Execute this on the App Server of the Source Environment
# Description:
# This script accepts source and destination credentials to access SMB shares of 2 servers (SMB must be accessible)
# An copy of a backup from Source to Destination SQL servers will be the result if credentials, services, and firewall ports are working as required
# The function assumes that it's being invoked on the Source App Server
# And that the Source App Server has WinRM & DB connections to its associated SQL Node
function addUserToLocalGroup{
param(
$computername=$env:computername,
$localAdminCred,
[string[]]$accountToAdd,
$localGroup='Administrators'
)
try{
$session=new-pssession $computername -Credential $localAdminCred -ea Stop
}
catch{
write-warning "Unable to connect to WinRM of $computername"
return $false
}
invoke-command -session $session -scriptblock{
param($principleName,$groupName)
$members=get-localgroupmember $groupName
if(!($principleName -in $members.Name)){
try{
write-host "Adding $principleName into $groupName";
Add-LocalGroupMember -Group $groupName -Member $principleName -ea Stop;
$currentMembers=get-localgroupmember $groupName|ft|out-string
write-host "$principleName has been added to $groupName successfully:`r`n$currentMembers";
return $true
}
catch{
write-warning "$error"
return $false
}
}
else{
write-host "$principleName is already a member of $groupName."
return $true}
} -args $accountToAdd,$localGroup
remove-pssession $session
}
function shipDatabase{
param(
$databaseName,$dbData,$dbLog,$overwrite,
$sourceSqlServer,$sourceSa,$sourceSaPassword,
$destinationSqlServer,$destinationSa,$destinationSaPassword
)
# Start the timer for this activity
$stopWatch= [System.Diagnostics.Stopwatch]::StartNew()
write-host "Shipping database $databaseName..."
function mountDriveAsUser($username,$password,$driveLetter,$uncPath){
if(get-psdrive $driveLetter -ea SilentlyContinue){
#Remove-PSDrive $firstAvailableDriveLetter -ea SilentlyContinue #This does not affect drives being mounted by 'net use' command
net use /delete ($driveLetter+':') 2>&1>null
}
try{
# This command cannot persist when out of scope of function; hence, net use is required
# New-PSDrive –Name $mountLetter –PSProvider FileSystem –Root $uncPath –Persist -Credential $mountAsCred|out-null
net use "$driveLetter`:" "$uncPath" /user:$username $password /persistent:Yes 2>&1>null
if(test-path "$driveLetter`:\"){
write-host "$driveLetter`: has successfully mounted.";
#return $true;
}
else{
write-host "Unable to mount drive $driveLetter";
#return $false
}
}
catch{
write-warning "$error"
#return $false
}
}
function triggerSqlBackup($sqlServer,$databaseName,$saCred){
$ErrorActionPreference='stop'
if(Test-NetConnection $sqlServer -CommonTCPPort WINRM){
try{
invoke-command -Credential $saCred -ComputerName $sqlServer -ScriptBlock{
param ($databaseName)
import-module sqlps
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') | out-null
$database=Get-SqlDatabase -ServerInstance $env:computername|?{$_.Name -eq $databaseName}
$success=Backup-SqlDatabase -DatabaseObject $database -CompressionOption On -CopyOnly
return $success
} -Args $databaseName
return $True
}catch{
Write-Warning $error[0].Exception.Message
return $false
}
}else{
try{
# Ensure the the Jump Box has SQL PowerShell tools
$moduleName='SqlServer'
if(!(Get-Module -ListAvailable -Name $moduleName -ea SilentlyContinue)){
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
if(!(Get-PackageProvider Nuget -ea SilentlyContinue)){
try{
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Install-PackageProvider -Name NuGet -Force -ErrorAction SilentlyContinue;
}catch{
write-warning $error[0].Exception.Message
}
}
try{
Install-Module -Name $moduleName -Force -Confirm:$false
# Install Chocolatey
if (!(Get-Command choco.exe -ErrorAction SilentlyContinue)) {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))}
# Defining $ENV:ChocotaleyInstall so that it would be called by refreshenv
$ENV:ChocolateyInstall = Convert-Path "$((Get-Command choco).Path)\..\.."
Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1"
Update-SessionEnvironment
}catch{
write-warning $error[0].Exception.Message
return $false
}
}
# Trigger backup of target database into the default backup directory
$database=Get-SqlDatabase -ServerInstance $sqlServer -credential $saCred|?{$_.Name -eq $databaseName}
Backup-SqlDatabase -DatabaseObject $database -credential $saCred -CompressionOption On -CopyOnly
return $true
}catch{
Write-Warning $error[0].Exception.Message
return $false
}
}
}
# Get default backup directory of a SQL server and mount it as the First Available Drive Letter
function convertLocalToUnc($localPath,$computername){
$uncPath=.{$x=$localPath -replace "^([a-zA-Z])\:","\\$computername\`$1`$";
if($x -match '\\$'){return $x.Substring(0,$x.length-1)}else{return $x}
}
$validLocal=if($localPath){test-path $localPath -ErrorAction SilentlyContinue}else{$false}
$validUnc=if($uncPath){test-path $uncPath -ErrorAction SilentlyContinue}else{$false}
if($validUnc){write-host "$uncPath is reachable, using session credentials of $(whoami)"}
else{write-host "Advisory: $computername is unreachable, using session credentials of $(whoami)"}
return $uncPath
}
function invokeGetDefaultSqlPaths($sqlServer,$saCredential){
$defaultValues=invoke-command -ComputerName $sqlServer -Credential $saCredential -ScriptBlock{
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') | out-null
$sqlConnection = New-Object ('Microsoft.SqlServer.Management.Smo.Server') $env:computername
$defaultBackupDirectory=$sqlConnection.Settings.BackupDirectory
$defaultDataDirectory=$sqlConnection.Settings.DefaultFile
$defaultLogDirectory=$sqlConnection.Settings.DefaultLog
return @($defaultDataDirectory,$defaultLogDirectory,$defaultBackupDirectory)
}
if($defaultValues){return $defaultValues}
else{return $false}
}
# Credentials
$sourceSaCred=New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $sourceSa,$(ConvertTo-securestring $sourceSaPassword -AsPlainText -Force)
$destinationSaCred=New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $destinationSa,$(ConvertTo-securestring $destinationSaPassword -AsPlainText -Force)
# Start the SQL dump into its default backup location
$backupSuccess=triggerSqlBackup $sourceSqlServer $databaseName $sourceSaCred
if(!$backupSuccess){
Write-Warning "Backup failed."
return $false
}
# Scan for next available drive letter, excluding D "CD Rom" and H "Home"
$unavailableDriveLetters=(Get-Volume).DriveLetter|sort
$availableDriveLetters=.{(65..90|%{[char]$_})|?{$_ -notin $unavailableDriveLetters}}
[char]$firstAvailableDriveLetter=$availableDriveLetters[0]
[char]$secondAvailableDriveLetter=$availableDriveLetters[1]
# Mount Source UNC path
$sourceUncPath=convertLocalToUnc $(invokeGetDefaultSqlPaths $sourceSqlServer $sourceSaCred)[2] $sourceSqlServer
mountDriveAsUser $sourceSA $sourceSaPassword $firstAvailableDriveLetter $sourceUncPath
$sourceFolder="$firstAvailableDriveLetter`:\"
# Mount Destination UNC path
$destinationDefaults=invokeGetDefaultSqlPaths $destinationSqlServer $destinationSaCred
$destinationUncPath=convertLocalToUnc $destinationDefaults[2] $destinationSqlServer
mountDriveAsUser $destinationSa $destinationSaPassword $secondAvailableDriveLetter $destinationUncPath
$destinationFolder="$secondAvailableDriveLetter`:\"
#if(!(test-path $destinationFolder)){mkdir $destinationFolder}
#$exportDirectory=$(split-path $backupFile -parent)
#$fileName=$(split-path $backupFile -leaf)
#$fileSize=(Get-Item $backupFile).length/1GB
#$destinationFile="$importDirectory\$fileName"
#if(get-item $destinationFile -ea SilentlyContinue){rm $destinationFile -Force}
# Give permissions to current user toward import directory, in case it's not already present
#[string]$currentAccount=$(whoami)
#$Acl = Get-Acl $destinationFolder
#$addPermission = New-Object System.Security.AccessControl.FileSystemAccessRule("$currentAccount", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow")
#$Acl.SetAccessRule($addPermission)
#Set-Acl $destinationFolder $Acl
#write-host (get-acl $destinationFolder|fl|out-string)
# Copy backup file to destination
$sourceBackupFile="$sourceFolder$databaseName.bak"
$sanityPassed=(test-path $sourceFolder) -and (test-path $destinationFolder) -and (test-path $sourceBackupFile)
if ($sanityPassed){
# rm $destinationFolder\*.* #Purge
write-host "Now copying $sourceBackupFile to $destinationFolder. Please wait..."
$fileSize=(get-item $sourceBackupFile).Length/1GB
$transferTime=(measure-command {robocopy "$sourceFolder" "$destinationFolder" "$databaseName.bak"}).TotalHours
$transferSpeed=[math]::round($fileSize/$transferTime,2)
write-host "$([math]::round($fileSize,2)) GB was copied in $([math]::round($transferTime,2)) hours => Speed: $transferSpeed GB/Hour"
}
# Rename the file to reflect its creation date stamp
#$pacificTime=[System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId( (Get-Date), 'Pacific Standard Time')
#$dateStamp = $pacificTime.tostring("MM-dd-yyyy-HHmm")
#Rename-Item -Path "$destinationFolder\$databaseName.bak" -NewName "$databaseName`_$dateStamp_PST.bak"
# This function currently only works for smaller databases due to timeout errors
# Execution Timeout Expired. The timeout period elapsed prior to completion of the operation or the server is not responding.
#The backup or restore was aborted.
#At line:16 char:9
#+ $dbImportResult=invoke-command -Session $session {
#+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# + CategoryInfo : InvalidOperation: (:) [Invoke-Sqlcmd], SqlPowerShellSqlExecutionException
# + FullyQualifiedErrorId : SqlError,Microsoft.SqlServer.Management.PowerShell.GetScriptCommand
# + PSComputerName : CRM-sql01.kimconnect.com
function invokeSqlImport{
param(
$sqlServer,
$saCred,
$databaseName,
$dataFile,
$logFile,
$backupFile,
$dbData='mscrm',
$dbLog='mscrm_log',
$overWrite
)
$ErrorActionPreference='stop'
try{
if(!$dataFile -or !$logFile -or !$backupFile){
$systemDefaults=.{
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') | out-null
$sqlConnection = New-Object ('Microsoft.SqlServer.Management.Smo.Server') $sqlServer
$defaultBackupDirectory=$sqlConnection.Settings.BackupDirectory
$defaultDataDirectory=$sqlConnection.Settings.DefaultFile
$defaultLogDirectory=$sqlConnection.Settings.DefaultLog
return @($defaultDataDirectory,$defaultLogDirectory,$defaultBackupDirectory)
}
if(!$dataFile){$dataFile = ($systemDefaults[0]+ "\$databaseName.mdf") -replace "\\{2}",'\'}
if(!$logFile){$logFile = ($systemDefaults[1]+ "\$databaseName.ldf") -replace "\\{2}",'\'}
if(!$backupFile){$backupFile="$($systemDefaults[2])\$databaseName.bak"}
}
#$maxTimeout=[int]::MaxValue
#$sessionOptions=New-PSSessionOption -OpenTimeOut $maxTimeout -OperationTimeout $maxTimeout
#$session=new-pssession $sqlServer -Credential $saCred -SessionOption $sessionOptions
$session=new-pssession $sqlServer -Credential $saCred
$dbImportResult=invoke-command -Session $session {
param($databaseName,$backupFile,$dataFile,$logFile,$dbData,$dbLog,$overwrite)
# Sanity check
if (!$databaseName -or !$backupFile -or !$dataFile -or !$logFile -or !$dbData -or !$dbLog){
write-warning "Please check the values and try again:`n
DatabaseName`t: $databaseName
BackupFile`t: $backupFile
DataFile`t: $dataFile
LogFile`t: $logFile
DbData`t: $dbData
DbLog`t: $dbLog
OverWriteFlag`t: $overwrite
"
return $false
}
if(!(test-path $backupFile)){
write-warning "$backupFile is invalid"
return $false
}
# Ensuring that this Server has SQL PowerShell tools
$moduleName='sqlps'
if(!(Get-Module -ListAvailable -Name $moduleName -ea SilentlyContinue)){
if(!('NuGet' -in (get-packageprovider).Name)){
try{
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction SilentlyContinue;
}
catch{
#Set-ItemProperty -Path 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\.NetFramework\v4.0.30319' -Name 'SchUseStrongCrypto' -Value '1' -Type DWord
#Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\.NetFramework\v4.0.30319' -Name 'SchUseStrongCrypto' -Value '1' -Type DWord
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction SilentlyContinue;
}
}
$null=Install-Module -Name $moduleName -Force -Confirm:$false
Update-SessionEnvironment
}
$null=import-module $moduleName
function confirmation($content,$testValue="I confirm",$maxAttempts=3){
$confirmed=$false;
$attempts=0;
$content|write-host
write-host "Please review this content for accuracy.`r`n"
while ($attempts -le $maxAttempts){
if($attempts++ -ge $maxAttempts){
write-host "A maximum number of attempts have reached. No confirmations received!`r`n"
break;
}
$userInput = Read-Host -Prompt "Please type in this value => $testValue <= to confirm. Input CANCEL to skip this item";
if ($userInput.ToLower() -eq $testValue.ToLower()){
$confirmed=$true;
write-host "Confirmed!`r`n";
break;
}elseif($userInput -like 'cancel'){
write-host 'Cancel command received.'
$confirmed=$false
break
}else{
cls;
$content|write-host
write-host "Attempt number $attempts of $maxAttempts`: $userInput does not match $testValue. Try again or Input CANCEL to skip this item`r`n"
}
}
return $confirmed;
}
# Trigger a restore of a database from the default backup directory
#$relocateData = New-Object Microsoft.SqlServer.Management.Smo.RelocateFile($dbData, "$dataFile")
#$relocateLog = New-Object Microsoft.SqlServer.Management.Smo.RelocateFile($dbLog, "$logFile")
#Restore-SqlDatabase -ServerInstance $sqlServer -Database $databaseName -BackupFile $backupFile -RelocateFile @($relocateData,$relocateLog)
# Preemptively resolve this error by killing all sessions:
#ALTER DATABASE failed because a lock could not be placed on database 'Test_MSCRM'. Try again later.
#ALTER DATABASE statement failed.
# + CategoryInfo : InvalidOperation: (:) [Invoke-Sqlcmd], SqlPowerShellSqlExecutionException
# + FullyQualifiedErrorId : SqlError,Microsoft.SqlServer.Management.PowerShell.GetScriptCommand
# + PSComputerName : SQL-TEST
$sqlRestoreExisting = "
USE [master]
DECLARE @killSessions varchar(4000) = '';
SELECT @killSessions = @killSessions + 'kill ' + CONVERT(varchar(5), spid) + ';'
FROM master..sysprocesses
WHERE dbid = db_id('$databaseName')
EXEC(@killSessions);
GO
ALTER DATABASE [$databaseName]
SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
GO
RESTORE DATABASE [$databaseName] FROM DISK = N'$backupFile' WITH FILE = 1,
MOVE N'$dbData' TO N'$dataFile',
MOVE N'$dbLog' TO N'$logFile',
NOUNLOAD, REPLACE, STATS = 5;
GO
ALTER DATABASE [$databaseName]
SET MULTI_USER;
GO
"
$sqlRestoreOverWrite = "
USE [master]
ALTER DATABASE [$databaseName]
SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
GO
RESTORE DATABASE [$databaseName] FROM DISK = N'$backupFile' WITH FILE = 1,
MOVE N'$dbData' TO N'$dataFile',
MOVE N'$dbLog' TO N'$logFile',
NOUNLOAD, REPLACE, STATS = 5;
GO
ALTER DATABASE [$databaseName]
SET MULTI_USER;
GO
"
$sqlForceRestore="
USE [master]
GO
RESTORE DATABASE [$databaseName]
FROM DISK = N'$backupFile'
WITH REPLACE, RECOVERY --force RESTORE
GO
"
$dbExists=(invoke-sqlcmd "SELECT CASE WHEN DB_ID('$databaseName') IS NULL THEN CAST(0 AS BIT) ELSE CAST(1 AS BIT) END").Column1
$maxQueryTimeout=[int]::MaxValue
if(!$dbExists){
write-host "$databaseName is now being added to $env:computername..."
$confirmed=confirmation -content $sqlRestoreOverWrite
if($confirmed){
try{
invoke-sqlcmd -Query $sqlRestoreOverWrite -QueryTimeout $maxQueryTimeout -ConnectionTimeout 0 -Verbose
}catch{
write-warning $error[0].Exception.Message
write-warning "Normal restore failed. Now forcing DB-restore sequence..."
$confirmForcedRestore=confirmation -content $sqlForceRestore
if($confirmForcedRestore){
invoke-sqlcmd $sqlForceRestore -QueryTimeout $maxQueryTimeout -ConnectionTimeout 0 -Verbose
}else{
write-warning "Forced-restore sequence has been declined. No changes were made."
}
}
}else{
write-host "Database import was not confirmed. No changes were made."
}
}
elseif($dbExists -and $overwrite){
write-host "$databaseName exists and over-write flag is True. Executing..."
$confirmed=confirmation -content $sqlRestoreExisting
if($confirmed){
try{
invoke-sqlcmd $sqlRestoreExisting -QueryTimeout $maxQueryTimeout -ConnectionTimeout 0 -Verbose
}
catch{
write-warning $error[0].Exception.Message
write-warning "Normal restory failed. Now forcing DB-restore sequence..."
$confirmForcedRestore=confirmation -content "Force restore using this T-SQL:`r`n$sqlForceRestore"
if($confirmForcedRestore){
invoke-sqlcmd $sqlForceRestore -QueryTimeout $maxQueryTimeout -ConnectionTimeout 0 -Verbose
}else{
write-warning "Database $databaseName was NOT successfully imported!"
}
}
}else{
write-warning "Database import was not confirmed. No changes were made."
}
}
else{
write-host "$databaseName currently Exists and over-write flag has been set as False"
}
# Validation
Function databaseExists{ param(
[Parameter(Mandatory=$true)][string]$sqlServer,
[Parameter(Mandatory=$true)][string]$dbName,
[Parameter(Mandatory=$false)][string]$port=1433,
[Parameter(Mandatory=$false)][string]$dbUser,
[Parameter(Mandatory=$false)][string]$dbPassword
)
$ErrorActionPreference='stop'
$dbExists = $false
try{
$moduleName='sqlps'
if(!(Get-Module -ListAvailable -Name $moduleName -ea SilentlyContinue)){
if(!('NuGet' -in (get-packageprovider).Name)){
try{
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction SilentlyContinue;
}
catch{
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction SilentlyContinue;
}
}
$null=Install-Module -Name $moduleName -Force -Confirm:$false
Update-SessionEnvironment
}
$null=import-module $moduleName
$conn = New-Object system.Data.SqlClient.SqlConnection
if($dbUser -and $dbPassword){
$conn.ConnectionString=[string]::format("Server={0};Database={1};User ID={2};Password={3};","$sqlServer,$port",$dbName,$dbUser,$dbPassword)
}else{
$conn.connectionstring=[string]::format("Server={0};Database={1};Integrated Security=SSPI;","$sqlServer,$port",$dbName)
#$conn.ConnectionString=”Server={0};Database={1};Integrated Security=True” -f $ServerInstance,$Database
}
$conn.open()
$conn.close()
return $true
}catch{
Write-Error $error[0].Exception.Message
return $false
}
}
$databaseExists=databaseExists $env:computername $databaseName
if ($databaseExists){
Set-location SQLSERVER:\SQL\$env:computername\DEFAULT
$currentDb=Get-SqlDatabase -Name $databaseName|select Name,RecoveryModel,CompatibilityLevel,CreateDate,DataSpaceUsage,LastBackupDate,Owner,PrimaryFilePath
return ($currentDb|out-string).Trim()
}else{
write-warning "Database $databaseName does not exist on $env:computername"
return $false
}
} -Args $databaseName,$backupFile,$dataFile,$logFile,$dbData,$dbLog,$overwrite
Remove-PSSession $session
return $dbImportResult
}catch{
Write-warning $error[0].Exception.Message
return $false
}
}
$importClock= [System.Diagnostics.Stopwatch]::StartNew()
$dataFile = ($destinationDefaults[0]+ "\$databaseName.mdf") -replace "\\{2}",'\'
$logFile = ($destinationDefaults[1]+ "\$databaseName.ldf") -replace "\\{2}",'\'
$destinationBackupFile="$($destinationDefaults[2])\$databaseName.bak"
$importResult=invokeSqlImport $destinationSqlServer $destinationSaCred $databaseName $dataFile $logFile $destinationBackupFile $dbData $dbLog $overwrite
write-host "Import result`: $importResult"
$importHours=$importClock.Elapsed.TotalHours;
$importClock.Stop();
# Cleanup Routine
if($importResult){
write-host "Import was successful!" -ForegroundColor Green
write-host "Cleaning up backup files at $destinationSqlServer..."
$null=remove-item -path $($destinationFolder+"$databaseName.bak") -force -ea SilentlyContinue
}else{
write-warning "Import was NOT successful.`r`nPlease manually import the $databaseName at $destinationSqlServer`r`nUsing backup file`t: $destinationBackupFile"
}
write-host "Cleaning up backup files at $sourceSqlServer..."
$null=remove-item -path $sourceBackupFile -force -ea SilentlyContinue
net use /delete $($firstAvailableDriveLetter+':') 2>&1>null
net use /delete $($secondAvailableDriveLetter+':') 2>&1>null
# Total duration
$hoursElapsed=$stopWatch.Elapsed.TotalHours;
$stopWatch.Stop();
$shippingResult="Database shipping result`r
----------------------------------------------`r
Export stats`r
------------`r
Source SQL Server`t: $sourceSqlServer`r
Backup successful`t: $backupSuccess`r
Database size`t: $([math]::round($fileSize,2)) GiB`r
File transfer duration`t: $([math]::round($transferTime,2)) hours`r
----------------------------------------------`r
Import stats`r
------------`r
Destination SQL Server`t: $destinationSqlServer`r
DB import duration`t: $([math]::round($importHours,2)) hours`r
Import Result`t: $(if($importResult){"`r+$importResult"}else{'Failed'})
----------------------------------------------`r
Overall stats`r
------------`r
Total time`t: $([math]::round($hoursElapsed,2)) hours`r
Aggregate speed`t: $([math]::round($hoursElapsed/$fileSize,2)) GB/Hour`r
----------------------------------------------"
return $shippingResult
}
function fixIsRequired{
param(
[Parameter(Mandatory=$true)][String[]]$sqlServer=$env:computername,
[Parameter(Mandatory=$true)][String[]]$databaseName,
[Parameter(Mandatory=$true)][String[]]$objectName,
[Parameter(Mandatory=$true)][String[]]$objectType,
[Parameter(Mandatory=$true)][String[]]$fromObject,
[Parameter(Mandatory=$true)]$saCred
)
# Validation
$validatedObjectName=!(checkDatabaseObject $sqlServer $databaseName $objectName $objectType $saCred)
$validatedModelObject=checkDatabaseObject $sqlServer $databaseName $fromObject $null $saCred
if(!$validatedObjectName -or !$validatedObjectType -or !$validatedModelObject){
write-warning "Some items does NOT require fixing:
New Object Not Exists`t: $objectName ($validatedObjectName)
Source Object`t: $fromObject ($validatedModelObject)
"
return $false
}else{
return $true
}
}
function checkDatabaseObject{
param(
[Parameter(Mandatory=$true)][String[]]$sqlServer=$env:computername,
[Parameter(Mandatory=$true)][String[]]$databaseName,
[Parameter(Mandatory=$true)][String[]]$objectName,
[Parameter(Mandatory=$false)][String[]]$objectType,
[Parameter(Mandatory=$false)]$saCred
)
$ErrorActionPreference='stop'
function includeSqlPs{
if(!(get-command invoke-sqlcmd)){
if(!('NuGet' -in (get-packageprovider).Name)){
try{
#Preempt this error: Unable to resolve package source 'https://www.powershellgallery.com/api/v2'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction SilentlyContinue;
#Resolve this error: PackageManagement\Install-Package : No match was found for the specified search criteria and module name 'sqlps'. Try Get-PSRepository to see all available registered module repositories.
#Register-PSRepository -Default
#Register-PSRepository -Name PSGallery -InstallationPolicy Trusted -Verbose
}
catch{
write-warning $error[0].Exception.Message
}
}
Install-Module -Name 'sqlserver' -Force -Confirm:$false
try{
Update-SessionEnvironment -ea stop
}
catch{
# Prempt these errors: Install Choco to peruse its prepackaged libraries
# Update-SessionEnvironment : The term 'Update-SessionEnvironment' is not recognized as the name of a cmdlet
# The term 'refreshenv' is not recognized as the name of a cmdlet, function, script file, or operable program
# Install Chocolatey
if (!(Get-Command choco.exe -ErrorAction SilentlyContinue)) {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))}
# Defining $ENV:ChocotaleyInstall so that it would be called by refreshenv
$ENV:ChocolateyInstall = Convert-Path "$((Get-Command choco).Path)\..\.."
Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1"
Update-SessionEnvironment
}
}
import-module 'SQLPS'
}
try{
$objectTypeAbbrevations=@{
'view'='V'
'procedure'='P'
'table'='U'
}
$sqlCheckCommand = if(!$objectType){"
USE [$databaseName]
GO
SELECT CASE
WHEN (OBJECT_ID('$objectName')) IS NULL
THEN CAST(0 AS BIT) ELSE CAST(1 AS BIT)
END
"
}else{"
USE [$databaseName]
GO
SELECT CASE
WHEN (OBJECT_ID('$objectName','$($objectTypeAbbrevations[$objectType])')) IS NULL
THEN CAST(0 AS BIT) ELSE CAST(1 AS BIT)
END
"
}
$objectExists=invoke-command -Credential $saCred -ComputerName $sqlServer -ScriptBlock{
param($includeSqlPs,$sqlCheckCommand)
[ScriptBlock]::Create($includeSqlPs).invoke()
return (invoke-sqlcmd $sqlCheckCommand -ServerInstance $env:computername).Column1
} -Args ${function:includeSqlPs},$sqlCheckCommand
return $objectExists
}catch{
# $ErrorActionPreference = "Stop" in conjunction with Write-Error = terminating error
write-error $error[0].Exception.Message
return $false
}
}
function tsqlCommandToCreateObject{
param(
[Parameter(Mandatory=$true)][String[]]$sqlServer=$env:computername,
[Parameter(Mandatory=$true)][String[]]$databaseName,
[Parameter(Mandatory=$true)][String[]]$objectName,
[Parameter(Mandatory=$true)][String[]]$objectType,
[Parameter(Mandatory=$true)][String[]]$fromObject,
[Parameter(Mandatory=$true)]$saCred
)
# Validate input
$validObjectTypes='view','procedure','table'
$validatedObjectType=!(!($validObjectTypes|?{$_ -eq $objectType}))
if(!$validatedObjectType){
write-warning "Object type $objectType is invalid."
return $null
}
$objectTypeAbbrevations=@{
'view'='V'
'procedure'='P'
'table'='U'
}
$prepTSql="
USE [$databaseName]
GO
DECLARE @sqlCmd nvarchar (1000000000)
BEGIN
IF (OBJECT_ID('$objectName', '$($objectTypeAbbrevations[$objectType])')) IS NULL
BEGIN
SELECT @sqlCmd = 'CREATE $objectType $objectName as SELECT * FROM $fromObject'
EXEC sp_executesql @sqlCmd
END
END
"
$tSql="
USE [$databaseName]
GO
DECLARE @sqlCmd nvarchar ($($prepTSql.Length +100))
BEGIN
IF (OBJECT_ID('$objectName', '$($objectTypeAbbrevations[$objectType])')) IS NULL
BEGIN
SELECT @sqlCmd = 'CREATE $objectType $objectName as SELECT * FROM $fromObject'
EXEC sp_executesql @sqlCmd
END
END
"
return $tSql
}
function invokeTsql{
param(
[Parameter(Mandatory=$true)][String[]]$sqlServer=$env:computername,
[Parameter(Mandatory=$true)][String[]]$databaseName,
[Parameter(Mandatory=$true)][String[]]$tSql,
[Parameter(Mandatory=$false)]$saCred
)
$ErrorActionPreference='stop'
function includeSqlPs{
if(!(get-command invoke-sqlcmd)){
if(!('NuGet' -in (get-packageprovider).Name)){
try{
#Preempt this error: Unable to resolve package source 'https://www.powershellgallery.com/api/v2'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction SilentlyContinue;
#Resolve this error: PackageManagement\Install-Package : No match was found for the specified search criteria and module name 'sqlps'. Try Get-PSRepository to see all available registered module repositories.
#Register-PSRepository -Default
#Register-PSRepository -Name PSGallery -InstallationPolicy Trusted -Verbose
}
catch{
write-warning $error[0].Exception.Message
}
}
Install-Module -Name 'sqlserver' -Force -Confirm:$false
try{
Update-SessionEnvironment -ea stop
}
catch{
# Prempt these errors: Install Choco to peruse its prepackaged libraries
# Update-SessionEnvironment : The term 'Update-SessionEnvironment' is not recognized as the name of a cmdlet
# The term 'refreshenv' is not recognized as the name of a cmdlet, function, script file, or operable program
# Install Chocolatey
if (!(Get-Command choco.exe -ErrorAction SilentlyContinue)) {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))}
# Defining $ENV:ChocotaleyInstall so that it would be called by refreshenv
$ENV:ChocolateyInstall = Convert-Path "$((Get-Command choco).Path)\..\.."
Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1"
Update-SessionEnvironment
}
}
import-module 'SQLPS'
}
try{
if ($saCred){
$session=new-pssession $sqlServer -Credential $saCred
}else{
$session=new-pssession $sqlServer
}
if(!$session){
write-warning "Unable to create a WinRM session toward $sqlServer"
return $false
}
$sqlExecResult=invoke-command -Session $session {
param($includeSqlPs,$databaseName,$tSql)
$ErrorActionPreference='stop'
function confirmation($content,$testValue="I confirm",$maxAttempts=3){
$confirmed=$false;
$attempts=0;
$content|write-host
write-host "Please review this content for accuracy.`r`n"
while ($attempts -le $maxAttempts){
if($attempts++ -ge $maxAttempts){
write-host "A maximum number of attempts have reached. No confirmations received!`r`n"
break;
}
$userInput = Read-Host -Prompt "Please type in this value => $testValue <= to confirm. Input CANCEL to skip this item";
if ($userInput.ToLower() -eq $testValue.ToLower()){
$confirmed=$true;
write-host "Confirmed!`r`n";
break;
}elseif($userInput -like 'cancel'){
write-host 'Cancel command received.'
$confirmed=$false
break
}else{
cls;
$content|write-host
write-host "Attempt number $attempts of $maxAttempts`: $userInput does not match $testValue. Try again or Input CANCEL to skip this item`r`n"
}
}
return $confirmed;
}
# Include SQL PowerShell tools
[ScriptBlock]::Create($includeSqlPs).invoke()
$dbExists=(invoke-sqlcmd "SELECT CASE WHEN DB_ID('$databaseName') IS NULL THEN CAST(0 AS BIT) ELSE CAST(1 AS BIT) END").Column1
if($dbExists){
$confirmed=confirmation "Please confirm this T-SQL statement. Ctrl+C to Cancel`r`n$tSql"
if($confirmed){
try{
invoke-sqlcmd -query $tSql
write-host "T-SQL has been committed successfully." -ForegroundColor Green
return $true
}catch{
write-host $error[0].Exception.Message -ForegroundColor Red
return $false
}
}else{
write-host "T-SQL has been cancelled by $(whoami)" -ForegroundColor Yellow
return $false
}
}else{
write-warning "Database $databaseName does not match any valid DB on $env:computername"
return $false
}
} -Args ${function:includeSqlPs},$databaseName,$tsql
Remove-PSSession $session
return $sqlExecResult
}catch{
Write-Warning $error[0].Exception.Message
return $false
}
}
# 8) Update Host Record on DNS
function updateHostRecordOnDns{
param(
$dnsServer,
$adminCred,
$record,
$ip,
$zone
)
try{
$session=if($adminCred){new-pssession -ComputerName $dnsServer -credential $adminCred}else{new-pssession -ComputerName $dnsServer}
$result=invoke-command -ComputerName $dnsServer -Session $session -ScriptBlock{
param($aRecord,$ip,$zone)
import-module dnsserver
$resolve=Get-DnsServerResourceRecord -Name $aRecord -ZoneName $zone -ea SilentlyContinue
if($resolve.HostName -ne $null){
$previousIp=$resolve.RecordData.IPv4Address.IPAddressToString
if($previousIp -ne $ip){
$newRecord = $resolve.Clone()
$newRecord.RecordData.IPv4Address=[ipaddress]$ip
Set-DnsServerResourceRecord -NewInputObject $newRecord -OldInputObject $resolve -ZoneName $zone -PassThru
$advisory="$aRecord previous ip $previousIp has been changed to $ip!"
write-warning $advisory
return $True
}
}else{
try{
Add-DnsServerResourceRecordA -Name $aRecord -IPv4Address $ip -ZoneName $zone -AllowUpdateAny -TimeToLive 01:00:00 -Confirm:$false -EA Stop
$msg="$aRecord $ip has been added to zone $zone on $env:computername"
write-host $msg -ForegroundColor Green
return $true
}
catch{
write-warning "Unable to add record $aRecord to DNS server $env:computername`r`r$($Error[0].Exception.Message)"
return $false
}
}
} -Args $record,$ip,$zone
remove-pssession $session
return $result
}catch{
write-warning $Error[0].Exception.Message
if($session){remove-pssession $session}
return $false
}
}
# 13) Validation
function testLogin{
param(
$url,
$username,
$password,
$usernameElementId='userNameInput',
$passwordElementId='passwordInput',
$submitButtonId='submitButton'
)
$ErrorActionPreference = "SilentlyContinue"
function killInternetExplorer{
$ieInstances=(New-Object -COM 'Shell.Application').Windows()|?{$_.Name -like '*Internet Explorer*'}
$ieInstances|%{$_.Quit()
[Runtime.Interopservices.Marshal]::ReleaseComObject($_)
}
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
}
killInternetExplorer|out-null
try{
$ie = New-Object -ComObject 'internetExplorer.Application'
$ie.Visible= $true # Make it visible
$ie.Navigate("$url")
while($ie.ReadyState -ne 4) {start-sleep -m 100}
try{
$usernamefield = $ie.Document.IHTMLDocument3_getElementById($usernameElementId)
$usernamefield.value = "$username"
$passwordfield = $ie.Document.IHTMLDocument3_getElementById($passwordElementId)
$passwordfield.value = "$password"
$ie.Document.IHTMLDocument3_getElementById($submitButtonId).click()
}
catch{
try{
$usernamefield = $ie.document.getElementByID($usernameElementId)
$usernamefield.value = "$username"
$passwordfield = $ie.document.getElementByID($passwordElementId)
$passwordfield.value = "$password"
$Link = $ie.document.getElementByID($submitButtonId).click
}
catch{
try{
$document = $ie.Document
$form = $document.forms[0]
$inputs = $form.getElementsByTagName("input")
($inputs | where {$_.name -eq "username"}).value = $username
($inputs | where {$_.name -eq "Password"}).value = $password
($inputs | where {$_.name -eq "Submit"}).click()
}
catch{
write-warning "Funky site you haz. Me can't login."
}
}
}
#$ieContent=$ie.document.documentelement.innerText
While ($ie.Busy -eq $true) {Start-Sleep -Seconds 1;}
$title=$ie.Document.Title
$not404=$title -notmatch '^404'
write-host "Page reached: $title"
$ie.Quit()
killInternetExplorer|out-null
return $not404
}
catch{
#write-warning "$Error"
killInternetExplorer|out-null
return $false
}
}
function autologinSe{
param(
$url,
$username,
$password,
$usernameElementId='userNameInput',
$passwordElementId='passwordInput',
$submitButtonId='submitButton',
$exitIeWhenDone=$false
)
$ErrorActionPreference = 'continue'
# Initial validation
if(!$url){write-warning "No URL specified.";return $false}
function killInternetExplorer{
$ieInstances=(New-Object -COM 'Shell.Application').Windows()|?{$_.Name -like '*Internet Explorer*'}
$ieInstances|%{$_.Quit()
[Runtime.Interopservices.Marshal]::ReleaseComObject($_)
}
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
}
function enableIeProtectedMode{
# $hives = 0..4|%{"HKLM:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\$_"}
$hives = 0..4|%{"HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\$_"}
$keyName='2500' # Key Name '2500' corresponds to 'Protected Mode' in IE
#Skipping zone 0 as that is the default local machine zone
$hives[1..4]|%{Set-ItemProperty -Path $_ -Name $keyName -Value 0}
$keys=$hives|%{Get-ItemProperty -Path $_}|select DisplayName, `
@{name='status';e={
if($_.$keyName -eq 0){'enabled'}
elseif($_.$keyName -eq 3){'disabled'}
else{'n/a'}
}}
write-host "IE Protected Mode Standardized Values:`r`n$($keys|out-string)"
}
function disableIeProtectedMode{
# $hives = 0..4|%{"HKLM:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\$_"}
$hives = 0..4|%{"HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\$_"}
$keyName='2500' # Key Name '2500' corresponds to 'Protected Mode' in IE
#Skipping zone 0 as that is the default local machine zone
$hives[1..4]|%{Set-ItemProperty -Path $_ -Name $keyName -Value 3}
$keys=$hives|%{Get-ItemProperty -Path $_}|select DisplayName, `
@{name='status';e={
if($_.$keyName -eq 0){'enabled'}
elseif($_.$keyName -eq 3){'disabled'}
else{'n/a'}
}}
write-host "IE Protected Mode Standardized Values:`r`n$($keys|out-string)"
}
function allowActiveX($zone='Trusted'){
#Source: https://learn.microsoft.com/en-us/previous-versions/troubleshoot/browsers/security-privacy/ie-security-zones-registry-entries
$zoneCode=switch($zone){
'My Computer'{0;break}
'Local Intranet'{1;break}
'Trusted'{2;break}
'Internet'{3;break}
'Restricted Sites'{4;break}
default{2}
}
#Reference table:
#Value Setting
#------------------------------
#0 My Computer
#1 Local Intranet Zone
#2 Trusted sites Zone
#3 Internet Zone
#4 Restricted Sites Zone
$hashMap=@{
'2702'=0 #ActiveX controls and plug-ins: Allow ActiveX Filtering = Enable (2702)
'1208'=0 #ActiveX controls and plug-ins: Allow previously unused ActiveX controls to run without prompt = Enable (1208)
'1209'=0 #ActiveX controls and plug-ins: Allow Scriptlets = Enable (1209)
'2201'=3 #ActiveX controls and plug-ins: Automatic prompting for ActiveX controls = Disable (2201)
'2000'=0 #ActiveX controls and plug-ins: Binary and script behaviors = Enable (2000)
'120A'=0 #Display video and animation on a webpage that does not use external media player = Enable (120A)
'1001'=0 #ActiveX controls and plug-ins: Download signed ActiveX controls = Enable (1001)
'1004'=0 #ActiveX controls and plug-ins: Download unsigned ActiveX controls = Enable (1004)
'1201'=0 #ActiveX controls and plug-ins: Initialize and script ActiveX controls not marked as safe for scripting = Enable (1201)
'120B'=3 #Only allow approved domains to use ActiveX without prompt = Disable (120B)
'1200'=0 #ActiveX controls and plug-ins: Run ActiveX controls and plug-ins = Enable (1200)
'1405'=0 #ActiveX controls and plug-ins: Script ActiveX controls marked as safe for scripting = Enable (1405)
}
$trustedDomains="HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\zones\$zoneCode"
$currentValues=Get-ItemProperty $trustedDomains
foreach ($item in $hashMap.GetEnumerator()) {
$key = $item.Key
$value = $item.Value
if($currentValues.$key -ne $value){
New-ItemProperty -Path $trustedDomains -Name $key -Value $value -PropertyType DWORD -Force
}
}
}
function addDomainToTrustedSites($url){
$httpType=.{[void]($url -match '^(https{0,1})');$matches[1]}
$domain=([uri]$url).Host
#$rootDomain=$domain.split('.')[-2..-1] -join '.' # This is assuming that the TLD is one-dotted (e.g. .com) not two-dotted (e.g. co.uk)
$rootDomain=.{$fragments=$domain.split('.')
$fragments[1..$($fragments.count)] -join '.'
}
write-host "Root domain detected`t: $rootDomain"
# The more advanced function to retrieve this value is at https://blog.kimconnect.com/powershell-extract-root-domain-from-url/
if ($rootDomain -notmatch '\.' -or $rootDomain -eq $env:USERDNSDOMAIN){
write-host "There's no need to add $url to the Trusted zone as it is local to this domain."
return $true
}
$dwordValue=2 # value of true correlates to 'enable'
$domainRegistryPath='HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\Domains'
$domainRegistryPath2='HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap\EscDomains' #EscDomains key applies to those protocols that are affected by the Enhanced Security Configuration (ESC)
$null=New-Item -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings\ZoneMap' -ItemType File -Name 'EscDomains' -Force
$null=New-Item -Path "$domainRegistryPath" -ItemType File -Name "$rootDomain" -Force
$null=New-Item -Path "$domainRegistryPath2" -ItemType File -Name "$rootDomain" -Force
$null=Set-ItemProperty -Path "$domainRegistryPath\$rootDomain" -Name $httpType -Value $dwordValue
$null=Set-ItemProperty -Path "$domainRegistryPath2\$rootDomain" -Name $httpType -Value $dwordValue
# Also add {about:blank} record as that doesn't seem to have been added by default
if (!(test-path "$domainRegistryPath\blank")){
#New-ItemProperty -Path $trustedDomains -Name $key -Value $value -PropertyType DWORD -Force
$null=New-Item -Path "$domainRegistryPath" -ItemType File -Name 'blank'
$null=Set-ItemProperty -Path "$domainRegistryPath\blank" -Name 'about' -Value $dwordValue
}
if (!(test-path "$domainRegistryPath2\blank")){
$null=New-Item -Path "$domainRegistryPath2" -ItemType File -Name 'blank'
$null=Set-ItemProperty -Path "$domainRegistryPath2\blank" -Name 'about' -Value $dwordValue
}
# Also add {about:internet} record since it will stop a login when missing
if (!(test-path "$domainRegistryPath\internet")){
$null=New-Item -Path "$domainRegistryPath" -ItemType File -Name 'internet'
$null=Set-ItemProperty -Path "$domainRegistryPath\internet" -Name 'about' -Value $dwordValue
}
if (!(test-path "$domainRegistryPath2\internet")){
$null=New-Item -Path "$domainRegistryPath2" -ItemType File -Name 'internet'
$null=Set-ItemProperty -Path "$domainRegistryPath2\internet" -Name 'about' -Value $dwordValue
}
$valueAfterChanged=(Get-ItemProperty "$domainRegistryPath\$rootDomain")."$httpType"
$value2AfterChanged=(Get-ItemProperty "$domainRegistryPath2\$rootDomain")."$httpType"
if ($valueAfterChanged -eq 2 -and $value2AfterChanged -eq 2 ){
write-host "$rootDomain has been added to Internet Explorer"
return $true
}
else{
write-warning "$rootDomain has NOT been added to Internet Explorer."
return $false
}
}
function includeSelenium{
Import-Module Selenium -ea SilentlyContinue
if (!(get-module selenium -EA SilentlyContinue)){
Start-job {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
if(!(Get-PackageProvider Nuget -ea SilentlyContinue)){Install-PackageProvider -Name NuGet -Force}
# Defining $ENV:ChocotaleyInstall so that it would be called by refreshenv
$ENV:ChocolateyInstall = Convert-Path "$((Get-Command choco).Path)\..\.."
Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1"
Install-Module Selenium -Force -Confirm:$False
} |Receive-Job -Wait
Update-SessionEnvironment
Import-Module Selenium
}
}
function invokeSelenium($url,$userName,$password,$usernameElementId,$passwordElementId,$submitButtonId){
$ErrorActionPreference = "Stop"
function closeSelenium($selenium){
if($selenium){
$selenium.close()
$selenium.quit()
}
}
function noLogin($url){
$seleniumIe = New-Object "OpenQA.Selenium.IE.InternetExplorerDriver"
$seleniumIe.Navigate().GoToURL($url)
$title=$seleniumIe.Title
write-host "Page reached: '$title'"
$trustedSiteError=$title -match '^Error'
if($trustedSiteError){
write-host "An site trust issue has been detected. Adding root domain to the trusted sites list to resolve this issue."
addDomainToTrustedSites $url
closeSelenium $seleniumIe
return $false
}
else{
return $seleniumIe
}
}
function login($url,$username,$password,$usernameElementId,$passwordElementId,$submitButtonId){
$seleniumIe = New-Object "OpenQA.Selenium.IE.InternetExplorerDriver"
$seleniumIe.Navigate().GoToURL($url)
$userField=$seleniumIe.FindElementById($usernameElementId)
$userField.clear()
$userField.SendKeys($username)
$passwordField=$seleniumIe.FindElementById($passwordElementId)
$passwordField.clear()
$passwordField.SendKeys($password)
$submitButton=$seleniumIe.FindElementById($submitButtonId)
$submitButton.Click()
$title=$seleniumIe.Title
write-host "Page reached: '$title'"
$trustedSiteError=$title -match '^Error'
if($trustedSiteError){
write-warning "A site trust issue has been detected."
closeSelenium $seleniumIe
return $false
}else{
return $seleniumIe
}
}
try{
$null=allowActiveX
$isLogin=$userName,$password,$usernameElementId,$passwordElementId,$submitButtonId|?{!(!$_)}
if($isLogin){
write-host "Login to $url as $userName..."
$ie=login $url $userName $password $usernameElementId $passwordElementId $submitButtonId
}else{
write-host "Accesing $url without login..."
$ie=nologin $url
}
return $ie
}
catch{
Write-Warning $Error[0].Exception.Message
return $false
}
}
try{
write-host "Username`t: $username`r`nPassword`t: $(!(!$password))`r`nusernameElementId`t: $usernameElementId`r`npasswordElementId`t: $passwordElementId`r`nsubmitButtonId`t: $submitButtonId"
$null=includeSelenium
$null=disableIeProtectedMode
$null=addDomainToTrustedSites $url
if(get-module selenium -ea SilentlyContinue){
$isLogin=$userName,$password,$usernameElementId,$passwordElementId,$submitButtonId|?{!(!$_)}
if($isLogin){
$selenium=invokeSelenium $url $userName $password $usernameElementId $passwordElementId $submitButtonId
}else{
write-host "No username or password are given. Proceeding to access only the provided URL."
$selenium=invokeSelenium $url
}
}else{
write-warning "Please manually verify that the Selenium module is installed before retrying this function."
}
if($selenium){
if($exitIeWhenDone){
$null=killInternetExplorer
return $true
}else{
return $selenium
}
}else{
write-warning "There were errors preventing a successful login."
return $false
}
}
catch {
write-warning "$_"
return $false
}
# Note on a common error:
#New-Object : Exception calling ".ctor" with "0" argument(s): "Unexpected error launching Internet Explorer. Protected
#Mode settings are not the same for all zones. Enable Protected Mode must be set to the same value (enabled or
#disabled) for all zones. (SessionNotCreated)"
#At line:1 char:15
#+ $seleniumIe = New-Object "OpenQA.Selenium.IE.InternetExplorerDriver"
#+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# + CategoryInfo : InvalidOperation: (:) [New-Object], MethodInvocationException
# + FullyQualifiedErrorId : ConstructorInvokedThrowException,Microsoft.PowerShell.Commands.NewObjectCommand
# Solution: either DISABLE or ENABLE Protected mode for ALL ZONES
}
# getEncryptionKey $dataEncryptionUrl $destinationCrmAdmin $destinationCrmPassword
function getEncryptionKey($dataEncryptionUrl,$username,$password){
$command="autologinSe '$dataEncryptionUrl' '$username' '$password'"
$ie=Invoke-Expression $command
#$ie.Navigate().GoToURL($dataEncryptionUrl)
$key=$ie.FindElementById('txtDisplayEncryptionKeyHidden').getAttribute('value')
$ie.close()
return $key
}
function setEncryptionKey($dataEncryptionUrl,$crmAdmin,$crmAdminPassword,$encryptionKey){
$command="autologinSe '$dataEncryptionUrl' '$crmAdmin' '$crmAdminPassword' -exitIeWhenDone `$false"
write-host $command
pause
$ie=Invoke-Expression $command
#$ie.Navigate().GoToURL($dataEncryptionUrl)
#$ie.FindElementById('txtDisplayEncryptionKeyHidden').getAttribute('value')
$encryptionKeyField=$ie.FindElementById('txtRestoreEnterEncryptionKey')
$activateButton=$ie.FindElementById('butRestoreEncryptionKey')
$encryptionKeyField.sendKeys($encryptionKey)
$activateButton.click()
$null=$activateButton.click()
return $true
# $showEncryptionCheckbox=$ie.FindElementById('chkShowEncryptionKey').click()
<#
$setKeyValue=$ie.FindElementById('txtDisplayEncryptionKeyHidden').getAttribute('value')
if($setKeyValue -eq $encryptionKey){
write-host "Encryption key has been set successfully" -ForegroundColor Green
return $true}
else{
write-warning "Encryption key set is $setKeyValue does not match the intended value of $encryptionKey"
return $false}
#>
}
function validateLogins{
param(
$url,$testUsers,$credDictionary
)
$validationResults=@{}
foreach ($user in $testUsers){
$testPassword=$credDictionary[$user]
$ie=autoLoginSe $url $user $testPassword -exitIeWhenDone $true
if($ie){$validationMessage="Success: $user is able to login to $url"}
else{$validationMessage="Failure: $user is NOT able to login to $url"}
write-host $validationMessage
$validationResults.Add($user,$validationMessage)
}
return $validationResults
}
# Import CRM
#function sendKeysToProgram($programExe,$programTitle,$sendkeys,$waitSeconds){
# $processes=get-process
# $processStarted=$programExe -in $processes.Path
# if($processStarted){
# $processMatch=$processes|?{$programExe -eq $_.Path}
# stop-process $processMatch -Force
# }
# start-process $programExe|out-null
# $wshell = New-Object -ComObject wscript.shell;
# $wshell.AppActivate($programTitle)|out-null
# sleep $waitSeconds
# $wshell.SendKeys($sendkeys)
# }
function importCrm{
param(
$destinationSqlServer,
$databasename,
$friendlyName,
$reportServer,
$mappingMethod='ByAccount'
)
$deploymentManagerExe="C:\Program Files\Dynamics 365\Tools\Microsoft.Crm.DeploymentManager.exe"
$deploymentManagerTitle='Dynamics 365 Deployment Manager'
$deploymentManagersendKeys='{DOWN 2}%{A}{DOWN}{ENTER}'
$deploymentManagersendKeys=5
try{
Add-PSSnapin Microsoft.Crm.PowerShell
Import-CrmOrganization -DatabaseName $databaseName `
-SqlServerName $destinationSqlServer `
-SrsUrl $reportServer `
-DisplayName $friendlyName `
-UserMappingMethod $mappingMethod `
-Verbose
}
catch{
write-warning "$error"
write-host "Please proceed to import $databasename via the GUI"
sendKeysToProgram $deploymentManagerExe $deploymentManagerTitle $deploymentManagersendKeys $deploymentManagersendKeys
}
}
# Recreate Email Router
# To be executed on Destination App server
# Retrieve the email router config from the DB shipping log prior to executing this
function startEmailRouterGui{
$emailRouterExecutable="C:\Program Files\Microsoft CRM Email\Service\Microsoft.Crm.Tools.Email.Management.exe"
$emailRouterTitle='Email Router Configuration Manager'
$emailRouterSendKeys="{TAB}{TAB}{ENTER}"
$emailRouterWaitSeconds=3
write-host "Now accessing Email Router Configuration Manager..."
accessEmailRouterConfigManager $emailRouterExecutable $emailRouterTitle $emailRouterSendKeys $emailRouterWaitSeconds
}
function outputFunctionText($function){
$functionObject=get-command $function
$functionName=$functionObject.Name
$functionBody=$functionObject.Definition
return "Function $functionName{$functionBody}"
}
function generateRandomPassword{
param(
$minLength = 25,
$maxLength = 40,
$nonAlphaChars = 2,
$excludeRegex='[:\$\%\&]',
$replaceExclusionWith=@(',',';','!','/','{','^','+','-','*','_')
)
add-type -AssemblyName System.Web
$randomLength = Get-Random -Minimum $minLength -Maximum $maxLength
$randomPassword = [System.Web.Security.Membership]::GeneratePassword($randomLength, $nonAlphaChars)
$sanitizedPassword = $randomPassword -replace $excludeRegex,"$(Get-Random -InputObject $replaceExclusionWith)"
$fixedRepeating = .{$rebuiltString=''
for ($i=0;$i -lt $sanitizedPassword.length;$i++){
$previousChar=$sanitizedPassword[$i-1]
$thisChar=$sanitizedPassword[$i]
$nextChar=$sanitizedPassword[$i+1]
if($thisChar -eq $nextChar){
do{
$regenChar=[char](Get-Random (65..122) )
}until($regenChar -ne $previousChar -and $regenChar -ne $nextChar)
$rebuiltString+=$regenChar
}
else{$rebuiltString+=$thisChar}
}
return $rebuiltString
}
return $fixedRepeating
}
function includePrerequisites{
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# Prempt these errors: Install Choco to peruse its prepackaged libraries
# Update-SessionEnvironment : The term 'Update-SessionEnvironment' is not recognized as the name of a cmdlet
# The term 'refreshenv' is not recognized as the name of a cmdlet, function, script file, or operable program
if (!(Get-Command choco.exe -ErrorAction SilentlyContinue)) {
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
# Defining $ENV:ChocotaleyInstall so that it would be called by refreshenv
}
$ENV:ChocolateyInstall = Convert-Path "$((Get-Command choco).Path)\..\.."
Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1"
if($PSVersionTable.PSVersion.Major -lt 5){choco install powershell -y}
Update-SessionEnvironment
if(!(Get-PackageProvider Nuget -ea SilentlyContinue)){Install-PackageProvider -Name NuGet -Force -confirm:$false}
if(!(Get-Module -ListAvailable -Name 'sqlserver' -ea SilentlyContinue)){Install-Module -Name 'sqlserver' -Force -Confirm:$false}
import-module selenium
if(!(get-module Selenium -ea SilentlyContinue)){Install-Module Selenium -Force -Confirm:$False}
}
function programCompletionWatcher{
param(
$targetMachine=$env:computername,
$exeName='Microsoft.Crm.MultiTenantPackageDeploymentExecutor', #$exeName='mmc'
$credential
)
$wizardWatcherClock=[System.Diagnostics.Stopwatch]::StartNew()
if($credential){
$destinationAppServerSession=New-PSSession $targetMachine -Credential $credential -SessionOption $(new-pssessionoption -IncludePortInSPN)
}else{
$destinationAppServerSession=New-PSSession $targetMachine -SessionOption $(new-pssessionoption -IncludePortInSPN)
}
if($destinationAppServerSession){
$checkCPU="Invoke-Command -Session `$destinationAppServerSession {return (Get-Process '$exeName' -EA SilentlyContinue).CPU}"
write-host "Checking CPU Usage..."
$sameMeasurementCount=0
$completionMarkerCount=10
$almostComplete=$false
$completionMarker=$false
$previousCpuConsumption=0
While (!$completionMarker) {
$currentCpuConsumption=invoke-expression $checkCPU -ea SilentlyContinue
write-host $currentCpuConsumption
$cpuConsumptionChanged=$currentCpuConsumption -gt $previousCpuConsumption
$exeStarted=$currentCpuConsumption -ne $null
if(!$exeStarted){
write-host "$exeName has not started." -NoNewline
}
elseif(!$cpuConsumptionChanged){
if ($sameMeasurementCount++ -ge $completionMarkerCount){
$almostComplete=$true
Write-Host "Almost complete: " -ForegroundColor Yellow -NoNewline
}
}elseif($cpuConsumptionChanged -and $almostComplete){
write-host "Completion marker reached!" -ForegroundColor Green -NoNewline
$completionMarker=$true
break
}
sleep -Seconds 10
$previousCpuConsumption=$currentCpuConsumption
}
Remove-PSSession $destinationAppServerSession
$minutesElapsed=$wizardWatcherClock.Elapsed.TotalMinutes
return $minutesElapsed
}else{
write-warning "Unable to open a WinRM session to $targetMachine.`r`nPlease monitor it's progress manually."
return $false
}
}
################################### The functions below will be dependent on GLOBAL variables ###################################
function performValidation{
# Validation Phase
# required values:
# Logins
write-host "Performing validation..."
$validationResults=validateLogins $destinationUrl $testUsers $credentials|out-string|%{$_.trim()}
appendContent $logFile "Validation:`r`n================================`r`n$(get-date)`t: test login results`r`n$validationResults"
# Encryption key
write-host "Checking encryption key..."
$existingEncryptionKey=getEncryptionKey $dataEncryptionUrl $destinationCrmAdmin $destinationCrmPassword
if($existingEncryptionKey -eq $originalEncryptionKey){
write-host "Encryption key matches."
appendContent $logFile "$(get-date)`t: encryption key validated as $originalEncryptionKey"
}else{
write-warning "Encryption keys DO NOT MATCH."
appendContent $logFile "$(get-date)`t: encryption keys NOT MATCHED.`r`nCurrent Key`t: $existingEncryptionKey v.s. Expected Key`t: $originalEncryptionKey"
}
# Email router
if($validateEmailRouter){
write-host "Checking email router at destination $destinationAppServer..."
$destinationAppServerSession=New-PSSession $destinationAppServer -Credential $adminCredential -SessionOption $(new-pssessionoption -IncludePortInSPN)
if($destinationAppServerSession){
while(($destinationEmailRouter|out-string) -ne ($validateEmailRouter|out-string)){
$destinationEmailRouter=invoke-command -session $destinationAppServerSession -scriptblock{
param($getEmailRouter,$orgName)
return [ScriptBlock]::Create($getEmailRouter).invoke($orgName);
} -Args ${function:getEmailRouter},$orgName|Select-Object -Property * -ExcludeProperty PSComputerName,RunspaceId,PSShowComputerName
$identicalEmailRouters=($destinationEmailRouter|out-string) -eq ($validateEmailRouter|out-string)
if($destinationEmailRouter -and $identicalEmailRouters){
$message='Source and destination email routers are identical.'
}else{
$message="Source and destination email routers settings are different.`r`n$($destinationEmailRouter|out-string).trim())`r`nV.S.`r`n"
$message+="$(($validateEmailRouter|out-string).trim())`r`nPlease reconfigure email router to match."
write-host $message
pause
}
}
appendContent $logFile "$(get-date)`t: $message"
Remove-PSSession $destinationAppServerSession
}
# Test sending email at destination
write-host "Simulating send-mail at Destination server $destinationAppServer..."
$destinationAppServerSession=New-PSSession $destinationAppServer -Credential $adminCredential -SessionOption $(new-pssessionoption -IncludePortInSPN)
if($destinationAppServerSession){
$emailUser=$validateEmailRouter.EmailUser
$emailPassword=$validateEmailRouter.emailPassword
$smtpServer=$validateEmailRouter.smtpServer
$port=$validateEmailRouter.port
$emailTo='[email protected]'
$cc=$null
$subject="Test Email to Validate SMTP"
$body="This is a test email.<br><br>Please disregard"
$testEmail=invoke-command -Session $destinationAppServerSession -ScriptBlock{
param($importedFunction,$a,$b,$c,$d,$e,$f,$g,$h)
[ScriptBlock]::Create($importedFunction).invoke($a,$b,$c,$d,$e,$f,$g,$h)
} -Args ${function:sendEmail},$EmailUser,$emailPassword,$emailTo,$cc,$subject,$body,$smtpServer,$port
appendContent $logFile "$(get-date)`t: sendmail test at $destinationAppServer result = $testEmail"
Remove-PSSession $destinationAppServerSession
}
$startEmailRouter="Please set the email router at: $destinationAppServer`r`n$($validateEmailRouter|out-string).trim())`r`nEmail Router URL`t: $emailRouterUrl`r`nContinue when that process has completed"
write-host $startEmailRouter -ForegroundColor Yellow
pause
appendContent $logFile "$(get-date)`t: Email routers have been recreated at the destination CRM server $destinationAppServer"
}else{
write-host 'No email routers to validate.'
}
# Simulate user email sending routines
write-host "Send test email from inside CRM..."
$ie=autologinSe $destinationUrl $destinationReportLoginId $destinationReportPassword
$ie.FindElementById('TabGQC').click()
$ie.FindElementById('4202').click()
}
function obtainOriginalEncryptionKey{
# Obtaining orignal encryption key
write-host "Obtaining Data Encryption key..."
return $(getEncryptionKey $dataEncryptionUrl $sourceCrmAdmin $sourceCrmPassword)
#write-host "Found`t: $originalEncryptionKey"
}
function obtainEmailRouter{
write-host "Checking Email Router..."
$emailRouterInfo=$originalEmailRouter=getEmailRouter $orgName
if ($originalEmailRouter){
while ($emailRouterInfo){
if ($emailRouterInfo){
write-host "Email router detected."
write-host "Now accessing Email Router Configuration Manager > Deployments > select the Org name $orgname > Disable"
$sendkeys='{RIGHT}{TAB}'
$programExecutable="C:\Program Files\Microsoft CRM Email\Service\Microsoft.Crm.Tools.Email.Management.exe"
$programTitle='Email Router Configuration Manager'
sendKeysToProgram $programExecutable $programTitle $sendKeys 5
write-host "Continue when email router has been disabled."
pause
$emailRouterInfo=getEmailRouter $orgName
}else{
appendContent $logFile "$(get-date)`t: Email router disabled."
}
}
}else{
write-host "No email routers detected."
}
return $originalEmailRouter
}
function disableThisOrg ($orgName){
write-host "Disabling Org $orgName..."
disableOrg $sourceAppServer $orgName $adminCredential $logFile
while ((getorg $orgname).State -eq 'Enabled'){
write-host "Org name $orgname is currently 'enabled.' Please manually ensure that $orgName is disabled via GUI" -ForegroundColor Yellow
pause
}
return $True
}
function exportDatabase{
addUserToLocalGroup $sourceSqlServer $adminCredential $sourceSa # Ensuring that SA has access to SQL servers
addUserToLocalGroup $destinationSqlServer $adminCredential $destinationSa
$shipDatabaseCommand="shipDatabase -databaseName $databaseName -dbData $dbData -dbLog $dbLog -overwrite $overwriteFlag `
-sourceSqlServer $sourceSqlServer -sourceSa $sourceSa -sourceSaPassword $sourceSaPassword `
-destinationSqlServer $destinationSqlServer -destinationSa $destinationSa -destinationSaPassword $destinationSaPassword"
write-host $shipDatabaseCommand
write-host "Press Ctrl+C To Cancel"
pause
$dbShipResult=shipDatabase $databaseName $dbData $dbLog $overwriteFlag `
$sourceSqlServer $sourceSa $sourceSaPassword `
$destinationSqlServer $destinationSa $destinationSaPassword
return $dbShipResult
}
function createMissingDbView{
# Set these variables
#$sqlServer=$destinationSqlServer
#$saCred=$destinationSaCred
#$databaseName
$objectName='[dbo].[PrivilegeObjectTypeCodeView]'
$objectType='view'
$fromObject='[dbo].[PrivilegeObjectTypeCodes]'
$orgVersion=$version
$requireAddMissingView=$version -lt [version]'9.0'
if(!$requireAddMissingView){
return "Migrating from Version $orgVersion does not require generating $objectName"
}
$port5985Open=Test-NetConnection $destinationSqlServer -CommonTCPPort WINRM -InformationLevel Quiet -ea SilentlyContinue
if ($port5985Open){
$fixIsRequired=fixIsRequired -sqlServer $destinationSqlServer `
-databaseName $databaseName `
-objectName $objectName `
-objectType $objectType `
-fromObject $fromObject `
-saCred $destinationSaCred
if($fixIsRequired){
$tsql=tsqlCommandToCreateObject -sqlServer $destinationSqlServer `
-databaseName $databaseName `
-objectName $objectName `
-objectType $objectType `
-fromObject $fromObject `
-saCred $destinationSaCred
invokeTsql -sqlServer $destinationSqlServer `
-databaseName $databaseName `
-tSql $tsql `
-saCred $destinationSaCred
}else{
return "No fixes are required for $orgName"
}
}else{
return "Unable to create missing view due to WinRm connection failures."
}
}
function version8to9Fixes{
param(
$fromVersion='8.0',
$toVersion='9.0',
$destinationSqlServer,
$databaseName,
$objectName='[dbo].[PrivilegeObjectTypeCodeView]',
$objectType='view',
$fromObject='[dbo].[PrivilegeObjectTypeCodes]',
$destinationSaCred,
)
function tsqlCommandToCreateObject{
param(
[Parameter(Mandatory=$true)][String[]]$sqlServer=$env:computername,
[Parameter(Mandatory=$true)][String[]]$databaseName,
[Parameter(Mandatory=$true)][String[]]$objectName,
[Parameter(Mandatory=$true)][String[]]$objectType,
[Parameter(Mandatory=$true)][String[]]$fromObject,
[Parameter(Mandatory=$true)]$saCred
)
# Validate input
$validObjectTypes='view','procedure','table'
$validatedObjectType=!(!($validObjectTypes|?{$_ -eq $objectType}))
if(!$validatedObjectType){
write-warning "Object type $objectType is invalid."
return $null
}
$objectTypeAbbrevations=@{
'view'='V'
'procedure'='P'
'table'='U'
}
$prepTSql="
USE [$databaseName]
GO
DECLARE @sqlCmd nvarchar (max)
BEGIN
IF (OBJECT_ID('$objectName', '$($objectTypeAbbrevations[$objectType])')) IS NULL
BEGIN
SELECT @sqlCmd = 'CREATE $objectType $objectName as SELECT * FROM $fromObject'
EXEC sp_executesql @sqlCmd
END
END
"
$tSql="
USE [$databaseName]
GO
DECLARE @sqlCmd nvarchar (max)
BEGIN
IF (OBJECT_ID('$objectName', '$($objectTypeAbbrevations[$objectType])')) IS NULL
BEGIN
SELECT @sqlCmd = 'CREATE $objectType $objectName as SELECT * FROM $fromObject'
EXEC sp_executesql @sqlCmd
END
END
"
return $tSql
}
function invokeTsql{
param(
[Parameter(Mandatory=$true)][String[]]$sqlServer=$env:computername,
[Parameter(Mandatory=$true)][String[]]$databaseName,
[Parameter(Mandatory=$true)][String[]]$tSql,
[Parameter(Mandatory=$false)]$saCred
)
$ErrorActionPreference='stop'
function includeSqlPs{
if(!(get-command invoke-sqlcmd)){
if(!('NuGet' -in (get-packageprovider).Name)){
try{
#Preempt this error: Unable to resolve package source 'https://www.powershellgallery.com/api/v2'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction SilentlyContinue;
#Resolve this error: PackageManagement\Install-Package : No match was found for the specified search criteria and module name 'sqlps'. Try Get-PSRepository to see all available registered module repositories.
#Register-PSRepository -Default
#Register-PSRepository -Name PSGallery -InstallationPolicy Trusted -Verbose
}
catch{
write-warning $error[0].Exception.Message
}
}
Install-Module -Name 'sqlserver' -Force -Confirm:$false
try{
Update-SessionEnvironment -ea stop
}
catch{
# Prempt these errors: Install Choco to peruse its prepackaged libraries
# Update-SessionEnvironment : The term 'Update-SessionEnvironment' is not recognized as the name of a cmdlet
# The term 'refreshenv' is not recognized as the name of a cmdlet, function, script file, or operable program
# Install Chocolatey
if (!(Get-Command choco.exe -ErrorAction SilentlyContinue)) {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))}
# Defining $ENV:ChocotaleyInstall so that it would be called by refreshenv
$ENV:ChocolateyInstall = Convert-Path "$((Get-Command choco).Path)\..\.."
Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1"
Update-SessionEnvironment
}
}
import-module 'SQLPS'
}
try{
if ($saCred){
$session=new-pssession $sqlServer -Credential $saCred
}else{
$session=new-pssession $sqlServer
}
if(!$session){
write-warning "Unable to create a WinRM session toward $sqlServer"
return $false
}
$sqlExecResult=invoke-command -Session $session {
param($includeSqlPs,$databaseName,$tSql)
$ErrorActionPreference='stop'
function confirmation($content,$testValue="I confirm",$maxAttempts=3){
$confirmed=$false;
$attempts=0;
$content|write-host
write-host "Please review this content for accuracy.`r`n"
while ($attempts -le $maxAttempts){
if($attempts++ -ge $maxAttempts){
write-host "A maximum number of attempts have reached. No confirmations received!`r`n"
break;
}
$userInput = Read-Host -Prompt "Please type in this value => $testValue <= to confirm. Input CANCEL to skip this item";
if ($userInput.ToLower() -eq $testValue.ToLower()){
$confirmed=$true;
write-host "Confirmed!`r`n";
break;
}elseif($userInput -like 'cancel'){
write-host 'Cancel command received.'
$confirmed=$false
break
}else{
cls;
$content|write-host
write-host "Attempt number $attempts of $maxAttempts`: $userInput does not match $testValue. Try again or Input CANCEL to skip this item`r`n"
}
}
return $confirmed;
}
# Include SQL PowerShell tools
[ScriptBlock]::Create($includeSqlPs).invoke()
$dbExists=(invoke-sqlcmd "SELECT CASE WHEN DB_ID('$databaseName') IS NULL THEN CAST(0 AS BIT) ELSE CAST(1 AS BIT) END").Column1
if($dbExists){
$confirmed=confirmation "Please confirm this T-SQL statement. Ctrl+C to Cancel`r`n$tSql"
if($confirmed){
try{
invoke-sqlcmd -query $tSql
write-host "T-SQL has been committed successfully." -ForegroundColor Green
return $true
}catch{
write-host $error[0].Exception.Message -ForegroundColor Red
return $false
}
}else{
write-host "T-SQL has been cancelled by $(whoami)" -ForegroundColor Yellow
return $false
}
}else{
write-warning "Database $databaseName does not match any valid DB on $env:computername"
return $false
}
} -Args ${function:includeSqlPs},$databaseName,$tsql
Remove-PSSession $session
return $sqlExecResult
}catch{
Write-Warning $error[0].Exception.Message
return $false
}
}
$requireAddMissingView=$fromVersion -lt $toVersion
if(!$requireAddMissingView){
return "Migrating from Version $fromVersion does not require generating $objectName"
}else{
$tsql=tsqlCommandToCreateObject -sqlServer $destinationSqlServer `
-databaseName $databaseName `
-objectName $objectName `
-objectType $objectType `
-fromObject $fromObject `
-saCred $destinationSaCred
invokeTsql -sqlServer $destinationSqlServer `
-databaseName $databaseName `
-tSql $tsql `
-saCred $destinationSaCred
}
}
function migrateOrg{
#### Initializing
set-location C:\
setGlobalVariables
includePrerequisites
if(!$isPowerShellVersion5){write-warning "Detected PowerShell version $($PSVersionTable.PSVersion.Major) is does not meet requirements of this program. Many warnings will be thrown during runtime."}
#### Checkpoint
$confirmedProceed=confirmation "Are You Sure That $orgName Is To Be Disabled and Exported to $destinationSqlServer"
if (!$confirmedProceed){
write-host "$orgName migration cancelled."
return $false
}
#### Proceeding
$clock=[System.Diagnostics.Stopwatch]::StartNew()
#### Setting up log file
if(!(test-path $logFile)){
write-host "creating log file`t: $logFile"
if(test-path $(Split-Path $logFile -Parent)){New-Item -itemType File -Path $(Split-Path $logFile -Parent) -Name $(Split-Path $logFile -Leaf) -Force}
}
appendContent $logFile "$(get-date)`t: Migration Org name $orgName started by $(whoami)`r`n===============================================================`r`nOrg ID`t: $orgId"
#### Original Encryption Key - requires selenium & PowerShell 5.1+
$GLOBAL:originalEncryptionKey=obtainOriginalEncryptionKey
appendContent $logFile "Data Encryption Key`t: $(if($originalEncryptionKey){$originalEncryptionKey}else{'N/A'})"
#### Obtaining email Router
write-host "Checking Email Router..."
$GLOBAL:validateEmailRouter=obtainEmailRouter
if($validateEmailRouter){
appendContent $logFile "$(get-date)`t: Email Router Information checked`r`n-------------------------`r`n$(($validateEmailRouter|out-string).Trim())`r`n-------------------------"
}
#### Disable Org
disableThisOrg $orgName
appendContent $logFile "$(get-date)`t: Org $orgname disabled"
#### Metadata
write-host "Updating Federation Meta Data for $orgName..."
$sourceIfdResult=updateFederationMetadata $sourceAdfs $adminCredential $sourceEnvironment $sourceUrl
appendContent $logFile "$(get-date)`t: $sourceIfdResult"
#### Database shipping
$exportResult=exportDatabase
appendContent $logFile "$(get-date)`t: $exportResult"
#### V8 to V9 Considerations
$missingViewCreationResult=createMissingDbView
appendContent $logFile "$(get-date)`t: $missingViewCreationResult"
#### Email DB Shipping notifications
$databaseShipSubject=$emailSubject+"Dabase Export & Import"
$databaseShipDetails=$emailBody+$dbShipResult
sendEmail $emailUser $emailPass $sendEmailTo $copy $databaseShipSubject $databaseShipDetails
#### Updating Internal DNS
$dnsUpdateResult=updateHostRecordOnDns -dnsServer $dnsServer -adminCred $adminCredential -record $aRecord -ip $recordIp -zone $zoneName;
appendContent $logFile "$(get-date)`t: $dnsUpdateResult"
#### Updating External DNS
write-host "Please update the sub-domain host record $orgName with this $newPublicIP at: $publicDnsUrl`r`nContinue when that process has completed"
pause
appendContent $logFile "$(get-date)`t: $orgName old public ip of $oldPublicIp has been updated with new public IP of $newPublicIP at $publicDnsUrl"
#### Importing CRM at Destination
$importCrmCommmand="importCrm $destinationSqlServer $databasename '$friendlyName' $reportServer"
#$importCrmCode=$(outputFunctionText 'sendKeysToProgram')+"`r`n"+$(outputFunctionText 'importCrm')+"`r`n"+$importCrmCommmand
cls
write-host $importCrmCommmand
write-host "`r`nPlease use the code above to perform the CRM Import at the destination App server $destinationAppServer using this report server value $destinationReportingUrl`r`nThe DB must be set exactly as`t: $orgName`r`nPress Enter (Continue) when that process has been initiated."
pause
#### Check for CRM import completion
#$destinationAppServerSession=New-PSSession $destinationAppServer -Credential $destinationSaCred -SessionOption $(new-pssessionoption -IncludePortInSPN)
$wizardCompleted=programCompletionWatcher $destinationAppServer 'Microsoft.Crm.MultiTenantPackageDeploymentExecutor' $destinationSaCred
if($wizardCompleted){
# Email CRM Import completion
$crmImportCompletionSubject=$emailSubject+"CRM Import"
$crmImportCompletionBody=$emailBody+"Org $orgName import completed with a duration of $wizardCompleted minutes."
sendEmail $emailUser $emailPass $sendEmailTo $copy $crmImportCompletionSubject $crmImportCompletionBody
appendContent $logFile "$(get-date)`t: CRM Import has been completed at $destinationAppServer with a duration of $wizardCompleted minutes."
}else{
appendContent $logFile "$(get-date)`t: CRM Import has been completed at $destinationAppServer with a duration of unknown minutes."
}
#### Updating IDF at destination
$destinationIdfUpdateResult=invokeIfdUpdate $destinationAdfs $adminCredential $destinationEnvironment $destinationUrl
if($destinationIdfUpdateResult){
appendContent $logFile "$(get-date)`t: destination IDF result $destinationIdfUpdateResult"
}else{
$ifdErrorMessage="There's an error while updating IDF on $destinationAdfs"
write-warning $ifdErrorMessage
appendContent $logFile "$(get-date)`t: $ifdErrorMessage`r`n$destinationIdfUpdateResult"
}
pause
#### Disabling integration
write-host "Disabling integration for $orgName..."
$toReEnableIntegration=disableIntegration $integrationServer $adminCredential $integrationFile $matchString $logFile
#### Local host files
write-host "Updating Local Host Files on $sourceServers..."
$hostFileUpdateResults=invokeUpdateHostFiles $adminCredential $appLocalIp $orgName $domain $searchString $sourceServers $true $logFile
write-host $hostFileUpdateResults
appendContent $logFile "$(get-date)`t: Host files update results`r`n--------------------------$(($hostFileUpdateResults|out-string).trim())--------------------------"
#### Check if public DNS has been updated with the new IP
$resolvedPublicIp=(Resolve-DnsName $recordName).IpAddress
if($resolvedPublicIp -ne $newPublicIP){
write-warning "Cannot proceed while the $recordName resolved ip $resolvedPublicIp vs $newPublicIP don't match`r`nPlease verify that the public DNS server is set."
# Will try to wait for DNS update for 10 minutes
$retries=0
while ($resolvedPublicIp -ne $newPublicIP -and $retries -lt 10){
$retries++
write-host "$retries`t: Attempting to clear DNS cache..."
$dns1=(Get-WmiObject win32_networkadapterconfiguration | Where IPEnabled -eq $true).DNSServerSearchOrder[0]
$dns1ServerName=([System.Net.Dns]::GetHostByAddress($dns1).HostName)
invoke-command -Credential $adminCredential -ComputerName $dns1ServerName -ScriptBlock{Clear-DnsServerCache -Force;Clear-DnsClientCache}
Clear-DnsClientCache
$resolvedPublicIp=(Resolve-DnsName $recordName).IpAddress
write-host "Resolved: $resolvedPublicIp vs Expected: $newPublicIP"
start-sleep 60
}
# Temporarily set host file
updateHostFiles $newPublicIP $orgName $domain $searchString $sourceServers[0] $false
}else{
write-host "Public IP $resolvedPublicIp has been verified with destination environment successfully."
}
#### Setting encryption at Destination
write-host "Setting Data Encryption key..."
# some old keys are in Chinese characters, which will not display properly on the Shell window
$encryptionKey=if($originalEncryptionKey){$originalEncryptionKey}else{generateRandomPassword}
write-host "Now applying this password as Encryption key: $encryptionKey"
$setEncryptionCommand="setEncryptionKey '$dataEncryptionUrl' '$destinationCrmAdmin' '$destinationCrmPassword' '$encryptionKey'"
$successfulEncryption=Invoke-Expression $setEncryptionCommand
if(!$successfulEncryption){
write-host "Program is unable to set encryption key. Please run this command from a different computer`t:`r`n$setEncryptionCommand`r`n`r`nContinue at this terminal when done..."
pause
}
appendContent $logFile "$(get-date)`t: New encryption key has been set as $encryptionKey"
#### Check for whether there are custom reports
write-host "Checking for custom reports..."
write-host "Please manually check to see if there are reports and download them, if necessary.`r`nPlease copy this password before proceeding`r`nLogin as`t: $sourceReportLoginId`r`nPassword`t: $sourceReportPassword"
pause
$null=autologinSe $sourceReportingUrl
pause
write-host "Please manually check to see if there are reports to, if necessary.`r`nPlease copy this password before proceeding`r`nLogin as`t: $destinationReportLoginId`r`nPassword`t: $destinationReportPassword"
pause
$null=autologinSe $destinationReportingUrl
pause
appendContent $logFile "$(get-date)`t: custom reports checked."
#$ie=autologinSe $sourceReportingUrl # this is broken due to IE auth popups
#$hasReports=.{$ErrorActionPreference='stop'
# try{
# $ie.FindElementByCssSelector("a[href*='report/']")
# return $true
# }catch{
# return $false
# }
# }
#if($hasReports){
# $reportMessage="Custom reports are detected."
# write-host $reportMessage -ForegroundColor Yellow
# write-host "`r`nPlease ensure that they are downloaded before proceeding" -ForegroundColor Yellow -NoNewline
# pause
# write-host "Please import these custom reports by opening this link on $destinationAppServer`t: $destinationReportingUrl`r`nResume when that has been completed" -ForegroundColor Yellow -NoNewline
# pause
# }else{
# $reportMessage="No custom reports to download."
# write-host $reportMessage
# }
#if($ie){try{$ie.close()}catch{}}
performValidation
# Documentation
write-host "Please manually update documentation at $documentationUrl using this information:`r`nName`t: $friendlyName`r`nURL`t: $sourceUrl`r`nOrgID`t: $orgId`r`n-------------------------------`r`n"
pause
write-host "Please access $secRepoUrl to create this pin: $encryptionKey"
pause
appendContent $logFile "$(get-date)`t: Encryption key, POD IPs, and SSL Information Documentation has been updated."
#### Reanable Integration
if($toReEnableIntegration){
enableIntegration $integrationServer $adminCredential $integrationFile $matchString $logFile
}
#### Wrapping up
$totalHours=[math]::round($clock.Elapsed.TotalHours,2)
$clock.stop()
$completionMsg="$(get-date)`t: $orgName Migration completed.`r`nMigration total duration`t: $totalHours hours`r`n==============================================================="
write-host $completionMsg
appendContent $logFile $completionMsg
# Email migration results
$migrationResultSubject=$emailSubject+"Migration Results"
$migrationResultBody=$emailBody+ "Org $orgName migration."
sendEmail $emailUser $emailPass $sendEmailTo $copy $migrationResultSubject $migrationResultBody -attachments $logFile
Return $true
}
######################################################
migrateOrg
Categories: