539 lines
15 KiB
PowerShell
539 lines
15 KiB
PowerShell
|
|
trap {
|
|
&$TrapHandler
|
|
}
|
|
|
|
function Install-SqlServerPowerShellModule {
|
|
param (
|
|
[String] $SetupRoot = ''
|
|
)
|
|
begin {
|
|
Show-InvocationInfo $MyInvocation
|
|
}
|
|
end {
|
|
Show-InvocationInfo $MyInvocation -End
|
|
}
|
|
process {
|
|
trap {
|
|
&$TrapHandler
|
|
}
|
|
|
|
if ((Get-Module SQLPS -ListAvailable) -ne $null) {
|
|
Write-Log "Module SQLSP already installed."
|
|
return
|
|
}
|
|
|
|
if ($MuranoFileShare -eq '') {
|
|
$MuranoFileShare = [String]([Environment]::GetEnvironmentVariable('MuranoFileShare'))
|
|
if ($MuranoFileShare -eq '') {
|
|
throw "Unable to find MuranoFileShare path."
|
|
}
|
|
}
|
|
Write-LogDebug "MuranoFileShare = '$MuranoFileShare'"
|
|
|
|
if ($SetupRoot -eq '') {
|
|
$SetupRoot = [IO.Path]::Combine("$MuranoFileShare", 'Prerequisites\SQL Server\Tools')
|
|
}
|
|
Write-LogDebug "SetupRoot = '$SetupRoot'"
|
|
|
|
$FileList = @(
|
|
'SQLSysClrTypes.msi',
|
|
'SharedManagementObjects.msi',
|
|
'PowerShellTools.msi'
|
|
)
|
|
|
|
foreach ($MsiFile in $FileList) {
|
|
Write-Log "Trying to install '$MsiFile' ..."
|
|
$MsiPath = Join-Path $SetupRoot $MsiFile
|
|
if ([IO.File]::Exists($MsiPath)) {
|
|
Write-Log "Starting msiexe ..."
|
|
$Result = Exec -FilePath "msiexec.exe" -ArgumentList @('/i', "`"$MsiPath`"", '/quiet') -PassThru
|
|
if ($Result.ExitCode -ne 0) {
|
|
throw "Installation of MSI package '$MsiPath' failed with error code '$($Result.ExitCode)'"
|
|
}
|
|
}
|
|
else {
|
|
Write-Log "File '$MsiPath' not found."
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function Install-SqlServerForAOAG {
|
|
param (
|
|
# Path to folder where msi files for additional SQL features are located
|
|
[String] $SetupRoot = '',
|
|
|
|
# Path to folder where msi files for additional SQLPS module are located
|
|
[String] $SqlpsSetupRoot = '',
|
|
|
|
[String] $MuranoFileShare = '',
|
|
|
|
# (REQUIRED) Domain name
|
|
[String] $SQLServiceUserDomain = 'fc-acme.local',
|
|
|
|
# (REQUIRED) User name for the account which will be used by SQL service
|
|
[String] $SQLServiceUserName = 'Administrator',
|
|
|
|
# (REQUIRED) Password for that user
|
|
[String] $SQLServiceUserPassword = 'P@ssw0rd',
|
|
|
|
[Switch] $UpdateEnabled
|
|
)
|
|
begin {
|
|
Show-InvocationInfo $MyInvocation
|
|
}
|
|
end {
|
|
Show-InvocationInfo $MyInvocation -End
|
|
}
|
|
process {
|
|
trap {
|
|
&$TrapHandler
|
|
}
|
|
|
|
if ($MuranoFileShare -eq '') {
|
|
$MuranoFileShare = [String]([Environment]::GetEnvironmentVariable('MuranoFileShare'))
|
|
if ($MuranoFileShare -eq '') {
|
|
throw "Unable to find MuranoFileShare path."
|
|
}
|
|
}
|
|
Write-LogDebug "MuranoFileShare = '$MuranoFileShare'"
|
|
|
|
if ($SetupRoot -eq '') {
|
|
$SetupRoot = [IO.Path]::Combine("$MuranoFileShare", 'Prerequisites\SQL Server\2012')
|
|
}
|
|
Write-LogDebug "SetupRoot = '$SetupRoot'"
|
|
|
|
$ExtraOptions = @{}
|
|
|
|
if ($UpdateEnabled) {
|
|
$ExtraOptions += @{'UpdateEnabled' = $true}
|
|
}
|
|
else {
|
|
$ExtraOptions += @{'UpdateEnabled' = $false}
|
|
}
|
|
|
|
$null = New-SQLServerForAOAG `
|
|
-SetupRoot $SetupRoot `
|
|
-SQLSvcUsrDomain $SQLServiceUserDomain `
|
|
-SQLSvcUsrName $SQLServiceUserName `
|
|
-SQLSvcUsrPassword $SQLServiceUserPassword `
|
|
-ExtraOptions $ExtraOptions
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function Initialize-AlwaysOnAvailabilityGroup {
|
|
param (
|
|
[String] $DomainName,
|
|
[String] $DomainAdminAccountName,
|
|
[String] $DomainAdminAccountPassword,
|
|
[String] $SqlServiceAccountName,
|
|
[String] $PrimaryNode,
|
|
[String] $ShareName = 'SharedWorkDir'
|
|
)
|
|
begin {
|
|
Show-InvocationInfo $MyInvocation
|
|
}
|
|
end {
|
|
Show-InvocationInfo $MyInvocation -End
|
|
}
|
|
process {
|
|
trap {
|
|
&$TrapHandler
|
|
}
|
|
|
|
$ShareNetworkPath = '\\' + $PrimaryNode + '\' + $ShareName
|
|
|
|
$DomainAdminAccountCreds = New-Credential `
|
|
-UserName "$DomainName\$DomainAdminAccountName" `
|
|
-Password "$DomainAdminAccountPassword"
|
|
|
|
$FunctionsFile = Export-Function 'Get-NextFreePort', 'Initialize-AlwaysOn'
|
|
|
|
$null = Start-PowerShellProcess @"
|
|
trap {
|
|
`$_
|
|
exit 1
|
|
}
|
|
|
|
Import-Module CoreFunctions
|
|
|
|
Write-Log "Importing functions file '$FunctionsFile' ..."
|
|
. "$FunctionsFile"
|
|
|
|
Write-Log "Starting 'Initialize-AlwaysOn' ..."
|
|
`$XmlFile = [IO.Path]::Combine("$ShareNetworkPath", "`$(`$Env:ComputerName).xml")
|
|
Write-Log "Output XML file is '`$XmlFile'"
|
|
Initialize-AlwaysOn | Export-CliXml -Path `$XmlFile
|
|
"@ -Credential $DomainAdminAccountCreds -NoBase64
|
|
}
|
|
}
|
|
|
|
|
|
function New-SharedFolderForAOAG {
|
|
param (
|
|
# (OPTIONAL)
|
|
[String] $SharePath = [IO.Path]::Combine($Env:SystemDrive + '\', 'SharedWorkDir'),
|
|
|
|
# (OPTIONAL)
|
|
[String] $ShareName = 'SharedWorkDir',
|
|
|
|
[String] $PrimaryNode = ' '
|
|
)
|
|
begin {
|
|
Show-InvocationInfo $MyInvocation
|
|
}
|
|
end {
|
|
Show-InvocationInfo $MyInvocation -End
|
|
}
|
|
process {
|
|
trap {
|
|
&$TrapHandler
|
|
}
|
|
|
|
if ($PrimaryNode.ToLower() -ne ($Env:ComputerName).ToLower()) {
|
|
Write-Log "This script runs on primary node only."
|
|
Write-Log "Exiting script."
|
|
return
|
|
}
|
|
|
|
if ($ShareName -eq '') {
|
|
$ShareName = [IO.Path]::GetFileNameWithoutExtension($SharePath)
|
|
}
|
|
|
|
Write-LogDebug "SharePath = '$SharePath'"
|
|
Write-LogDebug "ShareName = '$ShareName'"
|
|
|
|
try {
|
|
Write-LogDebug "Trying to remove share '$ShareName'"
|
|
$null = Get-SmbShare -Name $ShareName -ErrorAction 'Stop'
|
|
$null = Remove-SmbShare -Name $ShareName -Force
|
|
write-Log "Share '$ShareName' removed."
|
|
}
|
|
catch {
|
|
Write-LogWarning "Share '$ShareName' not exists or cannot be deleted."
|
|
}
|
|
|
|
try {
|
|
Write-LogDebug "Trying to remove folder '$SharePath"
|
|
$null = Get-Item -Path $SharePath -ErrorAction 'Stop'
|
|
$null = Remove-Item -Path $SharePath -Recurse -Force
|
|
Write-Log "Folder '$SharePath' removed."
|
|
}
|
|
catch {
|
|
Write-LogWarning "Folder '$SharePath' not exists or cannot be deleted."
|
|
}
|
|
|
|
$null = New-Item -Path $SharePath -ItemType Container -Force
|
|
|
|
$null = New-SmbShare -Path $SharePath `
|
|
-Name $ShareName `
|
|
-FullAccess "Everyone" `
|
|
-Description "Shared folder for AlwaysOn Availability Group setup."
|
|
|
|
return '\\' + $Env:ComputerName + '\' + $ShareName
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function New-DatabaseForAOAG {
|
|
param (
|
|
[String] $DatabaseName,
|
|
[String] $DomainName,
|
|
[String] $UserName,
|
|
[String] $UserPassword
|
|
)
|
|
|
|
$Creds = New-Credential -UserName "$DomainName\$UserName" -Password "$UserPassword"
|
|
|
|
$FunctionsFile = Export-Function 'Invoke-SQLText', 'ConvertTo-SQLName', 'ConvertTo-SQLString', 'New-SQLDatabase'
|
|
|
|
Start-PowerShellProcess @"
|
|
trap {
|
|
`$_
|
|
exit 1
|
|
}
|
|
|
|
Import-Module CoreFunctions
|
|
|
|
Write-Log "Importing functions from file '$FunctionsFile' ..."
|
|
. "$FunctionsFile"
|
|
|
|
Write-Log "Starting 'New-SQLDatabase' ..."
|
|
New-SQLDatabase $DatabaseName
|
|
"@ -Credential $Creds -NoBase64
|
|
}
|
|
|
|
|
|
|
|
function Initialize-AOAGPrimaryReplica {
|
|
param (
|
|
# (OPTIONAL) Name of the new Availability Group. If not specified then default name will be used.
|
|
[String] $GroupName = 'MuranoAG',
|
|
|
|
# (REQUIRED) Nodes that will be configured as replica partners.
|
|
#[Parameter(Mandatory=$true)]
|
|
[String[]] $NodeList,
|
|
|
|
# (REQUIRED) Node name that will be primary for selected Availability Group
|
|
#[Parameter(Mandatory=$true)]
|
|
[String] $PrimaryNode,
|
|
|
|
# (REQUIRED) Database list that will be added to the Availability Group
|
|
#[Parameter(Mandatory=$true)]
|
|
[String[]] $DatabaseList,
|
|
|
|
# (REQUIRED) Listener name that will be used by clients to connect to databases in that AG
|
|
#[Parameter(Mandatory=$true)]
|
|
[String] $ListenerName = 'MuranoAG_Listener',
|
|
|
|
# (REQUIRED) IP address of the listener
|
|
#[Parameter(Mandatory=$true)]
|
|
[String] $ListenerIP,
|
|
|
|
[String] $ListenerIPMask = '255.255.255.0',
|
|
|
|
[String] $ListenerPort = '5023',
|
|
|
|
# Sync Mode Node List
|
|
[String[]] $SyncModeNodeList,
|
|
|
|
[String] $SharedWorkDir = 'SharedWorkDir',
|
|
|
|
[String] $CliXmlFile = '',
|
|
|
|
[String] $DomainName,
|
|
[String] $UserName,
|
|
[String] $UserPassword
|
|
)
|
|
begin {
|
|
Show-InvocationInfo $MyInvocation
|
|
}
|
|
end {
|
|
Show-InvocationInfo $MyInvocation -End
|
|
}
|
|
process {
|
|
trap {
|
|
&$TrapHandler
|
|
}
|
|
|
|
Write-Log "Primary node: '$($PrimaryNode.ToLower())'"
|
|
Write-Log "Current node: '$(($Env:ComputerName).ToLower())'"
|
|
|
|
if ($PrimaryNode.ToLower() -ne $($Env:ComputerName).ToLower()) {
|
|
Write-Log "This function works on PrimaryNode only."
|
|
Write-Log "Exiting."
|
|
return
|
|
}
|
|
|
|
if ($CliXmlFile -eq '') {
|
|
$ReplicaDefinitionList = @()
|
|
foreach ($Node in $NodeList) {
|
|
try {
|
|
$NodeEndpointPort = Import-CliXml -Path "\\$PrimaryNode\SharedWorkDir\$Node.xml"
|
|
}
|
|
catch {
|
|
Write-Log "Using default endpoint port 5022"
|
|
$NodeEndpointPort = 5022
|
|
}
|
|
|
|
$ReplicaDefinition = @{
|
|
"SERVER_INSTANCE" = "$Node";
|
|
"ENDPOINT_URL" = "TCP://${Node}:${NodeEndpointPort}";
|
|
"AVAILABILITY_MODE" = "ASYNCHRONOUS_COMMIT";
|
|
"FAILOVER_MODE"="MANUAL";
|
|
}
|
|
|
|
if ($SyncModeNodeList -contains $Node) {
|
|
Write-Log "$Node is in SyncModeNodeList"
|
|
$ReplicaDefinition['AVAILABILITY_MODE'] = "SYNCHRONOUS_COMMIT"
|
|
$ReplicaDefinition['FAILOVER_MODE'] = "AUTOMATIC"
|
|
}
|
|
else {
|
|
Write-Log "$Node is NOT in SyncModeNodeList"
|
|
}
|
|
|
|
$ReplicaDefinitionList += @($ReplicaDefinition)
|
|
}
|
|
|
|
$Preferences = @{}
|
|
|
|
$ListenerDefinition = @{
|
|
"NAME"=$ListenerName;
|
|
"PORT" = "$ListenerPort";
|
|
"STATIC" = "$ListenerIP/$ListenerIPMask"
|
|
}
|
|
|
|
$Parameters = @{
|
|
'WorkDir' = "\\$PrimaryNode\$SharedWorkDir";
|
|
'Name' = $GroupName;
|
|
'DatabaseNames' = $DatabaseList;
|
|
'ReplicaDefs' = $ReplicaDefinitionList;
|
|
'Preferences' = $Preferences;
|
|
'ListenerDef' = $ListenerDefinition;
|
|
}
|
|
|
|
$null = Remove-Item -Path "\\$PrimaryNode\SharedWorkDir\*" -Force
|
|
|
|
$CliXmlFile = [IO.Path]::GetTempFileName()
|
|
|
|
Write-LogDebug "CliXml file: '$CliXmlFile'"
|
|
|
|
$null = Export-CliXml -Path $CliXmlFile -InputObject $Parameters -Depth 10
|
|
|
|
$null = Initialize-AOAGPrimaryReplica `
|
|
-CliXmlFile $CliXmlFile `
|
|
-DomainName $DomainName `
|
|
-UserName $UserName `
|
|
-UserPassword $UserPassword `
|
|
-PrimaryNode $PrimaryNode
|
|
|
|
Write-LogDebug "Inner 'Initialize-AOAGPrimaryReplica' call completed."
|
|
}
|
|
else {
|
|
$Creds = New-Credential -UserName "$DomainName\$UserName" -Password "$UserPassword"
|
|
|
|
$FunctionsFile = Export-Function -All
|
|
|
|
$null = Start-PowerShellProcess @"
|
|
trap {
|
|
`$_
|
|
exit 1
|
|
}
|
|
|
|
Import-Module CoreFunctions
|
|
|
|
Write-Log "Importing functions from '$FunctionsFile' ..."
|
|
. "$FunctionsFile"
|
|
|
|
Write-Log "Importing CliXml parameters file ..."
|
|
`$Parameters = Import-CliXml -Path $CliXmlFile
|
|
|
|
Write-Log "Starting 'New-AlwaysOnAvailabilityGroup' ..."
|
|
New-AlwaysOnAvailabilityGroup ``
|
|
-WorkDir `$Parameters['WorkDir'] ``
|
|
-Name `$Parameters['Name'] ``
|
|
-DatabaseNames `$Parameters['DatabaseNames'] ``
|
|
-ReplicaDefs `$Parameters['ReplicaDefs'] ``
|
|
-Preferences `$Parameters['Preferences'] ``
|
|
-ListenerDef `$Parameters['ListenerDef']
|
|
"@ -Credential $Creds -NoBase64
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function Initialize-AOAGSecondaryReplica {
|
|
param (
|
|
# (REQUIRED) Nodes that will be configured as replica partners.
|
|
[Parameter(Mandatory=$true)]
|
|
[String[]] $NodeList,
|
|
|
|
# (REQUIRED) Node name that will be primary for selected Availability Group
|
|
[Parameter(Mandatory=$true)]
|
|
[String] $PrimaryNode,
|
|
|
|
[String] $SharedWorkDir = 'SharedWorkDir',
|
|
|
|
[String] $DomainName,
|
|
[String] $UserName,
|
|
[String] $UserPassword
|
|
)
|
|
begin {
|
|
Show-InvocationInfo $MyInvocation
|
|
}
|
|
end {
|
|
Show-InvocationInfo $MyInvocation -End
|
|
}
|
|
process {
|
|
trap {
|
|
&$TrapHandler
|
|
}
|
|
|
|
if ($PrimaryNode.ToLower() -eq ($Env:ComputerName).ToLower()) {
|
|
Write-Log "This function works on any SecondaryNode only."
|
|
Write-Log "Exiting."
|
|
return
|
|
}
|
|
|
|
$Creds = New-Credential -UserName "$DomainName\$UserName" -Password "$UserPassword"
|
|
|
|
$FunctionsFile = Export-Function -All
|
|
|
|
$null = Start-PowerShellProcess @"
|
|
trap {
|
|
$_
|
|
exit 1
|
|
}
|
|
|
|
Import-Module CoreFunctions
|
|
|
|
Write-Log "Importing functions from '$FunctionsFile' ..."
|
|
. "$FunctionsFile"
|
|
|
|
Write-Log "Starting 'New-AlwaysOnAvailabilityGroupReplica' ..."
|
|
New-AlwaysOnAvailabilityGroupReplica -WorkDir "\\$PrimaryNode\$SharedWorkDir"
|
|
"@ -Credential $Creds -NoBase64
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function Disable-Firewall {
|
|
begin {
|
|
Show-InvocationInfo $MyInvocation
|
|
}
|
|
end {
|
|
Show-InvocationInfo $MyInvocation -End
|
|
}
|
|
process {
|
|
trap {
|
|
&$TrapHandler
|
|
}
|
|
|
|
netsh advfirewall set allprofiles state off
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function Enable-Firewall {
|
|
begin {
|
|
Show-InvocationInfo $MyInvocation
|
|
}
|
|
end {
|
|
Show-InvocationInfo $MyInvocation -End
|
|
}
|
|
process {
|
|
trap {
|
|
&$TrapHandler
|
|
}
|
|
|
|
netsh advfirewall set allprofiles state on
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function Enable-TrustedHosts {
|
|
begin {
|
|
Show-InvocationInfo $MyInvocation
|
|
}
|
|
end {
|
|
Show-InvocationInfo $MyInvocation -End
|
|
}
|
|
process {
|
|
trap {
|
|
&$TrapHandler
|
|
}
|
|
|
|
Set-Item WSMan:\localhost\Client\TrustedHosts -Value '*' -Force
|
|
}
|
|
}
|