
* Resolve issues with SQLCluster Ic2f5f1763472f13aeab63ef8919a437837d76b2f * Resolve issue with SQLCluster clusterIp property I2103edf9702b51472c32a3e5e789421fa1167c13 * Resolve issue with SQLServer Fixed bug #1245835 Id1adb038926f244ffbed6339a3aa1eb8387f29a5 * Linux+Apache support added, small fixes done I15db406d75e6e47ac2a7ffeb1e84da4a814d49f3 * Fixed linux workflow,templates and shell scripts Ie5ab1bc824dba07771d94686af63f939ab05d5ca * Remove send-command from Demo.xml I48a2ab0995c96c255c9dc95d84937427ae0b6194 * Changed config path and xinet dist based exec Ic03a540526e5807a43d07b70f901ce04dc5615f9 * Fixed FW saving rules on reboot for CentOS Iba4dbd4f5924f16dc6f5b547663157da189793c3 Change-Id: Iba4dbd4f5924f16dc6f5b547663157da189793c3
1374 lines
48 KiB
PowerShell
1374 lines
48 KiB
PowerShell
Import-Module NetSecurity
|
|
|
|
function Test-Key([string]$path, [string]$key) {
|
|
if(!(Test-Path $path)) { return $false }
|
|
if ((Get-ItemProperty $path).$key -eq $null) { return $false }
|
|
return $true
|
|
}
|
|
|
|
function Resolve-SQLServerPrerequisites {
|
|
<#
|
|
.SYNOPSIS
|
|
Installs MS SQL Server prerequisites (.Net Framework 3.5)
|
|
|
|
.DESCRIPTION
|
|
Installs MS SQL Server prerequisites (.Net Framework 3.5)
|
|
|
|
#>
|
|
if (-not (Test-Key "HKLM:\Software\Microsoft\NET Framework Setup\NDP\v3.5" "Install")) {
|
|
Import-Module ServerManager
|
|
Write-Host ".Net Framework 3.5 not found. Installing it using Server Manager..."
|
|
$Feature = Get-WindowsFeature NET-Framework
|
|
if ($Feature -eq $null) {
|
|
# We are probably on Windows Server 2012
|
|
$Feature = Get-WindowsFeature NET-Framework-Core
|
|
}
|
|
if (-not $Feature) {
|
|
throw ".Net framework 3.5 feature was not found."
|
|
}
|
|
if (-not $Feature.DisplayName -match "3.5") {
|
|
Log-Warning ".Net framework 3.5 is required, but $($Feature.DisplayName) is available as Windows feature. Proceeding with installation"
|
|
}
|
|
[void](Add-WindowsFeature $Feature)
|
|
}
|
|
}
|
|
|
|
function New-SQLServer {
|
|
<#
|
|
.SYNOPSIS
|
|
Installs new MS SQL Server instance. Returns $true if a reboot is required after the installation,
|
|
$false if a reboot is not required and throws an exception in case if installation fails.
|
|
|
|
.DESCRIPTION
|
|
Installs new MS SQL Server instance in unattended mode.
|
|
|
|
.PARAMETER SetupRoot
|
|
MS SQL Server installation files root directory. Normally it is just DVD drive name.
|
|
|
|
.PARAMETER ExtraFeatures
|
|
List of features to be installed in addition to default "SQLEngine", "Conn", "SSMS", "ADV_SSMS".
|
|
#>
|
|
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[string]$SetupRoot,
|
|
[array]$ExtraFeatures = @(),
|
|
[Hashtable]$ExtraOptions = @{}
|
|
)
|
|
|
|
$SetupDir = Get-Item $SetupRoot
|
|
$SetupExe = $SetupDir.GetFiles("setup.exe")[0]
|
|
|
|
Resolve-SQLServerPrerequisites
|
|
|
|
$parser = New-OptionParserInstall
|
|
$ExitCode = $parser.ExecuteBinary($SetupExe.FullName, @{"Q" = $null; "FEATURES" = @("SQLEngine", "Conn", "SSMS", "ADV_SSMS") + $ExtraFeatures} + $ExtraOptions)
|
|
|
|
if ($ExitCode -eq 3010) {
|
|
return $true
|
|
}
|
|
|
|
if ($ExitCode -ne 0) {
|
|
throw "Installation executable exited with code $("{0:X8}" -f $ExitCode) (Decimal: $ExitCode)"
|
|
}
|
|
|
|
return $false
|
|
}
|
|
|
|
function New-SQLServerForAOAG {
|
|
<#
|
|
.SYNOPSIS
|
|
Installs new MS SQL Server instance with all needed features to set up AlwaysOn Availability Group.
|
|
Returns $true if a reboot is required after the installation, $false if a reboot is not required
|
|
and throws an exception in case if installation fails.
|
|
|
|
.DESCRIPTION
|
|
Installs new MS SQL Server instance in unattended mode. All features for AlwaysOn Availability Groups are
|
|
installed.
|
|
|
|
All availability group members must be installed with the same SQLSvcUsrDoman, SQLSvcUsrName and SQLSvcUsrPassword parameters.
|
|
User must be a domain user since it will be used for nodes interconnection.
|
|
|
|
.PARAMETER SetupRoot
|
|
MS SQL Server installation files root directory. Normally it is just DVD drive name.
|
|
|
|
.PARAMETER SQLSvcUsrDomain
|
|
MS SQL Server user account domain name.
|
|
|
|
.PARAMETER SQLSvcUsrName
|
|
MS SQL Server user account name.
|
|
|
|
.PARAMETER SQLSvcUsrPassword
|
|
MS SQL Server user account password.
|
|
|
|
.PARAMETER ExtraFeatures
|
|
List of features to be removed besides "SQLEngine", "Conn", "SSMS", "ADV_SSMS", "DREPLAY_CTLR", "DREPLAY_CLT".
|
|
#>
|
|
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[string]$SetupRoot,
|
|
[parameter(Mandatory = $true)]
|
|
[string]$SQLSvcUsrDomain,
|
|
[parameter(Mandatory = $true)]
|
|
[string]$SQLSvcUsrName,
|
|
[parameter(Mandatory = $true)]
|
|
[string]$SQLSvcUsrPassword,
|
|
[array]$ExtraFeatures = @(),
|
|
[Hashtable]$ExtraOptions = @{}
|
|
)
|
|
|
|
$SetupDir = Get-Item $SetupRoot
|
|
$SetupExe = $SetupDir.GetFiles("setup.exe")[0]
|
|
|
|
$SQLUser = "$SQLSvcUsrDomain\$SQLSvcUsrName"
|
|
$domain = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$SQLSvcUsrDomain", $SQLSvcUsrName, $SQLSvcUsrPassword)
|
|
|
|
if ($domain.name -eq $null) {
|
|
throw "Credentials validation failed for user $SQLUser. Check domain, login name and password."
|
|
}
|
|
|
|
Resolve-SQLServerPrerequisites
|
|
|
|
$parser = New-OptionParserInstall
|
|
$ExitCode = $parser.ExecuteBinary($SetupExe.FullName, @{"Q" = $null; "FEATURES" = @("SQLEngine", "Conn", "SSMS", "ADV_SSMS", "DREPLAY_CTLR", "DREPLAY_CLT") + $ExtraFeatures;
|
|
"AGTSVCACCOUNT" = $SQLUser; "AGTSVCPASSWORD" = $SQLSvcUsrPassword; "ASSVCACCOUNT" = $SQLUser; "ASSVCPASSWORD" = $SQLSvcUsrPassword; "ASSYSADMINACCOUNTS" = $SQLUSer;
|
|
"SQLSVCACCOUNT" = $SQLUser; "SQLSVCPASSWORD" = $SQLSvcUsrPassword; "SQLSYSADMINACCOUNTS" = $SQLUser; "ISSVCACCOUNT" = $SQLUser; "ISSVCPASSWORD" = $SQLSvcUsrPassword;
|
|
"RSSVCACCOUNT" = $SQLUser; "RSSVCPASSWORD" = $SQLSvcUsrPassword} + $ExtraOptions)
|
|
|
|
if ($ExitCode -eq 3010) {
|
|
return $true
|
|
}
|
|
|
|
if ($ExitCode -ne 0) {
|
|
throw "Installation executable exited with code $("{0:X8}" -f $ExitCode) (Decimal: $ExitCode)"
|
|
}
|
|
|
|
return $false
|
|
}
|
|
|
|
function Remove-SQLServer {
|
|
<#
|
|
.SYNOPSIS
|
|
Uninstalls MS SQL Server instance installed with New-SQLServer cmdlet
|
|
|
|
.DESCRIPTION
|
|
Uninstalls MS SQL Server instance installed with New-SQLServer cmdlet in unattended mode
|
|
|
|
.PARAMETER SetupRoot
|
|
MS SQL Server installation files root directory. Normally it is just DVD drive name.
|
|
|
|
.PARAMETER ExtraFeatures
|
|
List of features to be removed besides "SQLEngine", "Conn", "SSMS", "ADV_SSMS".
|
|
#>
|
|
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[string]$SetupRoot,
|
|
[array]$ExtraFeatures = @()
|
|
)
|
|
|
|
$SetupDir = Get-Item $SetupRoot
|
|
$SetupExe = $SetupDir.GetFiles("setup.exe")[0]
|
|
|
|
$parser = New-OptionParserUninstall
|
|
$ExitCode = $parser.ExecuteBinary($SetupExe.FullName, @{"Q" = $null; "FEATURES" = @("SQLEngine", "Conn", "SSMS", "ADV_SSMS") + $ExtraFeatures})
|
|
|
|
if ($ExitCode -ne 0) {
|
|
throw "Installation executable exited with code $("{0:X8}" -f $ExitCode)"
|
|
}
|
|
}
|
|
|
|
function Install-SQLServerForSysPrep {
|
|
<#
|
|
.SYNOPSIS
|
|
Installs new MS SQL Server in sysprep mode.
|
|
|
|
.DESCRIPTION
|
|
Installs new MS SQL Server in sysprep mode. Returns $true if a reboot is required after the installation,
|
|
$false if a reboot is not required and throws an exception in case if installation fails.
|
|
|
|
Setup must be completed after booting rearmed machine by using Complete-SQLServer cmdlet
|
|
|
|
.PARAMETER SetupRoot
|
|
MS SQL Server installation files root directory. Normally it is just DVD drive name.
|
|
|
|
.PARAMETER ExtraFeatures
|
|
List of features to be installed in addition to default "SQLEngine". Note that prior to
|
|
SQL Server version 2012 Service Pack 1 Cumulative Update 2 (January 2013) only "Replication",
|
|
"FullText" and "RS" may be installed in addition to "SQLEngine". See the following link for
|
|
detials: http://msdn.microsoft.com/en-us/library/ms144259.aspx
|
|
|
|
#>
|
|
}
|
|
|
|
function Install-SQLServerForSysPrep {
|
|
<#
|
|
.SYNOPSIS
|
|
Installs new MS SQL Server in sysprep mode.
|
|
|
|
.DESCRIPTION
|
|
Installs new MS SQL Server in sysprep mode. Returns $true if a reboot is required after the installation,
|
|
$false if a reboot is not required and throws an exception in case if installation fails.
|
|
|
|
Setup must be completed after booting rearmed machine by using Complete-SQLServer cmdlet
|
|
|
|
.PARAMETER SetupRoot
|
|
MS SQL Server installation files root directory. Normally it is just DVD drive name.
|
|
|
|
.PARAMETER ExtraFeatures
|
|
List of features to be installed in addition to default "SQLEngine". Note that prior to
|
|
SQL Server version 2012 Service Pack 1 Cumulative Update 2 (January 2013) only "Replication",
|
|
"FullText" and "RS" may be installed in addition to "SQLEngine". See the following link for
|
|
detials: http://msdn.microsoft.com/en-us/library/ms144259.aspx
|
|
|
|
#>
|
|
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[string]$SetupRoot,
|
|
[array]$ExtraFeatures = @()
|
|
)
|
|
|
|
$SetupDir = Get-Item $SetupRoot
|
|
$SetupExe = $SetupDir.GetFiles("setup.exe")[0]
|
|
|
|
Resolve-SQLServerPrerequisites
|
|
|
|
$parser = New-OptionParserPrepareImage
|
|
$ExitCode = $parser.ExecuteBinary($SetupExe.FullName, @{"QS" = $null; "FEATURES" = @("SQLEngine") + $ExtraFeatures })
|
|
|
|
if ($ExitCode -eq 3010) {
|
|
return $true
|
|
}
|
|
|
|
if ($ExitCode -ne 0) {
|
|
throw "Installation executable exited with code $("{0:X8}" -f $ExitCode) (Decimal: $ExitCode)"
|
|
}
|
|
|
|
return $false
|
|
}
|
|
|
|
function Complete-SQLServerAfterSysPrep {
|
|
<#
|
|
.SYNOPSIS
|
|
Completes previously prepared with "Install-SQLServerForSysPrep" MS SQL Server after the system was rearmed.
|
|
|
|
.DESCRIPTION
|
|
Completes previously prepared with "Install-SQLServerForSysPrep" MS SQL Server after the system was rearmed.
|
|
Returns $true if a reboot is required after the installation, $false if a reboot is not required and throws
|
|
an exception in case if installation fails.
|
|
|
|
Setup must be completed after booting rearmed machine by using Complete-SQLServer cmdlet
|
|
|
|
.PARAMETER SetupRoot
|
|
MS SQL Server installation files root directory. Normally it is just DVD drive name.
|
|
#>
|
|
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[string]$SetupRoot
|
|
)
|
|
|
|
$SetupDir = Get-Item $SetupRoot
|
|
$SetupExe = $SetupDir.GetFiles("setup.exe")[0]
|
|
|
|
$parser = New-OptionParserCompleteImage
|
|
$ExitCode = $parser.ExecuteBinary($SetupExe.FullName, @{"QS" = $null})
|
|
|
|
if ($ExitCode -eq 3010) {
|
|
return $true
|
|
}
|
|
|
|
if ($ExitCode -ne 0) {
|
|
throw "Installation executable exited with code $("{0:X8}" -f $ExitCode) (Decimal: $ExitCode)"
|
|
}
|
|
|
|
return $false
|
|
}
|
|
|
|
function ConvertTo-SQLString {
|
|
<#
|
|
.SYNOPSIS
|
|
Converts argument to a valid SQL string in quotes
|
|
|
|
.DESCRIPTION
|
|
Converts argument to a valid SQL string in quotes. The string may contain any characters.
|
|
See http://msdn.microsoft.com/en-us/library/ms179899.aspx
|
|
|
|
.PARAMETER S
|
|
String to convert
|
|
#>
|
|
param(
|
|
[parameter(Mandatory = $true, ValueFromPipeline = $true)]
|
|
[string]$S
|
|
)
|
|
|
|
return "'$($S -replace "'", "''")'"
|
|
}
|
|
|
|
function ConvertTo-SQLName {
|
|
<#
|
|
.SYNOPSIS
|
|
Converts argument to a valid SQL name in brackets
|
|
|
|
.DESCRIPTION
|
|
Converts argument to a valid SQL name in brackets. The string may contain any characters.
|
|
See http://msdn.microsoft.com/en-us/library/ms175874.aspx
|
|
|
|
.PARAMETER S
|
|
String to convert
|
|
#>
|
|
param(
|
|
[parameter(Mandatory = $true, ValueFromPipeline = $true)]
|
|
[string]$S
|
|
)
|
|
return "[$($S -replace "]", "]]")]"
|
|
}
|
|
|
|
function Invoke-SQLText {
|
|
<#
|
|
.SYNOPSIS
|
|
Invokes SQL text
|
|
|
|
.DESCRIPTION
|
|
Invokes SQL text. Returns raw SQL server output.
|
|
|
|
.PARAMETER SQL
|
|
SQL Text
|
|
|
|
.PARAMETER User
|
|
SQL Server user name
|
|
|
|
.PARAMETER Password
|
|
SQL Server user password
|
|
#>
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[string]$SQL,
|
|
[string]$User = $null,
|
|
[string]$Password = $null
|
|
)
|
|
|
|
#Write-Warning "$SQL`n"
|
|
#return
|
|
|
|
$Binary = Get-Command "sqlcmd.exe"
|
|
|
|
$tempFile = [IO.Path]::GetTempFileName()
|
|
$tempFile = Get-Item $tempFile
|
|
Set-Content -Path $tempFile -Value $SQL
|
|
|
|
$CommandLine = @('-h', '-1', '-b', '-i', "`"$($tempFile.FullName)`"")
|
|
if (($User -ne $null) -and ($User -ne '')) {
|
|
$CommandLine = $CommandLine + '-U'
|
|
$CommandLine = $CommandLine + $User
|
|
$CommandLine = $CommandLine + '-P'
|
|
$CommandLine = $CommandLine + $Password
|
|
}
|
|
|
|
Write-Debug "Executing: `n$SQL`n"
|
|
[string]$output = &$Binary $CommandLine
|
|
|
|
$ExitCode = $LastExitCode
|
|
if ($ExitCode -ne 0) {
|
|
Write-Warning $output
|
|
throw "SQLCMD.EXE returned with exit code $ExitCode while running $Binary $CommandLine"
|
|
}
|
|
|
|
Remove-Item $tempFile
|
|
|
|
return $output
|
|
}
|
|
|
|
function New-SQLUser {
|
|
<#
|
|
.SYNOPSIS
|
|
Invokes SQL text
|
|
|
|
.DESCRIPTION
|
|
Invokes SQL text
|
|
|
|
.PARAMETER SQL
|
|
SQL Text
|
|
|
|
.PARAMETER User
|
|
SQL Server user name
|
|
|
|
.PARAMETER Password
|
|
SQL Server user password
|
|
#>
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[string]$SQL,
|
|
[string]$User = $null,
|
|
[string]$Password = $null
|
|
)
|
|
}
|
|
|
|
function New-Password {
|
|
<#
|
|
.SYNOPSIS
|
|
Creates random password of the specified length
|
|
|
|
.DESCRIPTION
|
|
Password contains random characters a-z, A-Z, numbers and special characters.
|
|
There is no guarantee that all the types of symbols will be present in the password.
|
|
|
|
.PARAMETER Length
|
|
Desired length of the password.
|
|
|
|
#>
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[int]$Length=6
|
|
)
|
|
|
|
$Result = ""
|
|
$alpha = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()'`"``_+[]\{}|;:,./<>?~"
|
|
while ($Length -gt 0) {
|
|
$x = Get-Random $alpha.Length
|
|
$c = $alpha[$x]
|
|
$Result = "$Result$c"
|
|
$Length = $Length - 1
|
|
}
|
|
return $Result
|
|
}
|
|
|
|
function Initialize-MirroringEndpoint {
|
|
<#
|
|
.SYNOPSIS
|
|
Creates mirroring endpoint.
|
|
|
|
.DESCRIPTION
|
|
Master key is created if necessary. Host certificate is created when necessary either (normally on first endpoint creation).
|
|
|
|
Endpoint and certificate are recreated in case if master key did not existed (should not normally happen).
|
|
|
|
Endpoint is recreated in case if certificate did not existed (should not happen unless the endpoint was created manually).
|
|
|
|
Mirroring endpoint is created unless one already exists. The endpoint is created with the specified name. When the endpoint
|
|
already exists is is unchanged.
|
|
|
|
Endpoint port is selected automatically as 4022 or as first available port after 4022 in case if 4022 is already listening.
|
|
If there is no firewall rule with name 'DatabaseMirroring-TCP-{portnumber}', allowing rule is created.
|
|
|
|
Certificate is stored in the specified file.
|
|
|
|
Returns endpoint listening port.
|
|
|
|
.PARAMETER EncryptionPassword
|
|
Encryption password used to create certificate.
|
|
|
|
.PARAMETER CertificateFileName
|
|
Certificate target file name. File MUST NOT exist.
|
|
|
|
#>
|
|
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[String]$EncryptionPassword,
|
|
[parameter(Mandatory = $true)]
|
|
[String]$CertificateFileName
|
|
)
|
|
|
|
$EndpointName = 'MirroringEndpoint'
|
|
|
|
$Folder = Get-Item $WorkDir
|
|
|
|
$H = $Env:COMPUTERNAME -replace '[^A-Za-z0-9_]', '_'
|
|
|
|
$Port = Get-NextFreePort 4022
|
|
|
|
$CreateMasterKey = "USE master;
|
|
|
|
IF NOT EXISTS(select * from sys.symmetric_keys where name = '##MS_DatabaseMasterKey##')
|
|
BEGIN
|
|
CREATE MASTER KEY ENCRYPTION BY PASSWORD = $(ConvertTo-SQLString $EncryptionPassword);
|
|
IF EXISTS(select * from sys.certificates where name = '${H}_cert')
|
|
BEGIN
|
|
DROP CERTIFICATE ${H}_cert
|
|
END
|
|
IF EXISTS(SELECT * FROM sys.endpoints WHERE type_desc='DATABASE_MIRRORING')
|
|
BEGIN
|
|
DECLARE `@name VARCHAR(255)
|
|
SELECT TOP 1 `@name = name FROM sys.endpoints WHERE type_desc='DATABASE_MIRRORING'
|
|
EXEC ('DROP ENDPOINT [' + `@name + ']')
|
|
END
|
|
END
|
|
GO
|
|
|
|
IF NOT EXISTS(select * from sys.certificates where name = '${H}_cert')
|
|
BEGIN
|
|
CREATE CERTIFICATE ${H}_cert WITH SUBJECT = '${H} endpoint certificate';
|
|
IF EXISTS(SELECT * FROM sys.endpoints WHERE type_desc='DATABASE_MIRRORING')
|
|
BEGIN
|
|
DECLARE `@name VARCHAR(255)
|
|
SELECT TOP 1 `@name = name FROM sys.endpoints WHERE type_desc='DATABASE_MIRRORING'
|
|
EXEC ('DROP ENDPOINT [' + `@name + ']')
|
|
END
|
|
END
|
|
GO
|
|
|
|
BACKUP CERTIFICATE ${H}_cert TO FILE = $(ConvertTo-SQLString "$CertificateFileName");
|
|
GO
|
|
|
|
DECLARE `@port int
|
|
IF EXISTS(SELECT * FROM sys.endpoints WHERE type_desc='DATABASE_MIRRORING')
|
|
BEGIN
|
|
SELECT `@port = port FROM sys.tcp_endpoints WHERE type_desc='DATABASE_MIRRORING'
|
|
END ELSE
|
|
BEGIN
|
|
CREATE ENDPOINT $(ConvertTo-SQLName $EndpointName)
|
|
STATE = STARTED
|
|
AS TCP (
|
|
LISTENER_PORT = $Port
|
|
, LISTENER_IP = ALL
|
|
)
|
|
FOR DATABASE_MIRRORING (
|
|
AUTHENTICATION = CERTIFICATE ${H}_cert
|
|
, ENCRYPTION = REQUIRED ALGORITHM AES
|
|
, ROLE = ALL
|
|
);
|
|
SELECT `@port = $Port
|
|
END
|
|
|
|
SELECT 'port:(' + CONVERT(VARCHAR, `@port) + ')' as port
|
|
GO
|
|
|
|
"
|
|
|
|
$rawdata = Invoke-SQLText -SQL $CreateMasterKey
|
|
[int]$Port = $rawdata -replace '.*port:\(([^)]*)\).*', '$1'
|
|
|
|
# Open port in Windows Firewall
|
|
|
|
$PortOpen = $false
|
|
$RuleName = "DatabaseMirroring-TCP-$Port"
|
|
Get-NetFirewallRule | Foreach-Object {
|
|
if ($_.Name -eq $RuleName) {
|
|
$PortOpen = $true
|
|
}
|
|
}
|
|
if (-not $PortOpen) {
|
|
$DisplayName = "MS SQL Database Mirroring Endpoint at TCP port $Port"
|
|
New-NetFirewallRule -Name $RuleName -DisplayName $DisplayName -Description $DisplayName -Protocol TCP -LocalPort $Port -Enabled True -Profile Any -Action Allow
|
|
}
|
|
|
|
return $Port
|
|
}
|
|
|
|
function Complete-MirroringEndpoint {
|
|
<#
|
|
.SYNOPSIS
|
|
Completes mirroring endpoint
|
|
|
|
.DESCRIPTION
|
|
Allows inbound connections from remote host
|
|
#>
|
|
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[String]$RemoteHostName,
|
|
[parameter(Mandatory = $true)]
|
|
[String]$RemoteWorkDir,
|
|
[String]$RemoteHostLogin,
|
|
[String]$RemoteHostUser,
|
|
[String]$RemoteHostPassword
|
|
)
|
|
|
|
$Folder = Get-Item $RemoteWorkDir
|
|
$RemoteWorkDir = $Folder.FullName
|
|
|
|
$H = $RemoteHostName -replace '[^A-Za-z0-9_]', '_'
|
|
|
|
if (-not $RemoteHostLogin) {
|
|
$RemoteHostLogin = "${H}_login"
|
|
}
|
|
if (-not $RemoteHostUser) {
|
|
$RemoteHostUser = "${H}_user"
|
|
}
|
|
if (-not $RemoteHostPassword) {
|
|
$RemoteHostPassword = "$(New-Password 10)aA#3"
|
|
}
|
|
|
|
$SQL = "USE master;
|
|
|
|
IF NOT EXISTS(select * from sys.sql_logins where name=$(ConvertTo-SQLString $RemoteHostLogin))
|
|
BEGIN
|
|
CREATE LOGIN $(ConvertTo-SQLName $RemoteHostLogin) WITH PASSWORD = $(ConvertTo-SQLString $RemoteHostPassword);
|
|
END
|
|
GO
|
|
|
|
IF NOT EXISTS(select * from sys.sysusers where name=$(ConvertTo-SQLString $RemoteHostUser))
|
|
BEGIN
|
|
CREATE USER $(ConvertTo-SQLName $RemoteHostUser) FOR LOGIN $(ConvertTo-SQLName $RemoteHostLogin);
|
|
END
|
|
GO
|
|
|
|
IF EXISTS(select * from sys.certificates where name='${H}_remote_cert')
|
|
BEGIN
|
|
DROP CERTIFICATE ${H}_remote_cert
|
|
END
|
|
GO
|
|
|
|
CREATE CERTIFICATE ${H}_remote_cert AUTHORIZATION $(ConvertTo-SQLName $RemoteHostUser) FROM FILE = $(ConvertTo-SQLString "$RemoteWorkDir\certificate.cer");
|
|
GO
|
|
|
|
DECLARE `@name VARCHAR(255)
|
|
SELECT TOP 1 `@name = name FROM sys.endpoints WHERE type_desc='DATABASE_MIRRORING'
|
|
SELECT 'name:(' + `@name + ')' as name
|
|
"
|
|
|
|
$rawdata = Invoke-SQLText -SQL $SQL
|
|
$EndpointName = $rawdata -replace '.*name:\(([^)]*)\).*', '$1'
|
|
$SQL = "GRANT CONNECT ON ENDPOINT::$(ConvertTo-SQLName $EndpointName) TO $(ConvertTo-SQLName $RemoteHostLogin)"
|
|
[void](Invoke-SQLText -SQL $SQL)
|
|
}
|
|
|
|
function Complete-SQLMirror {
|
|
<#
|
|
.SYNOPSIS
|
|
Completes creation of mirrored SQL database
|
|
|
|
.DESCRIPTION
|
|
This cmdlet should be first executed on mirror server and then on principal server.
|
|
Otherwise it will fail (however it may be executed again with no harm).
|
|
#>
|
|
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[String]$RemoteHostName,
|
|
[parameter(Mandatory = $true)]
|
|
[Int]$RemotePort,
|
|
[parameter(Mandatory = $true)]
|
|
[String]$DatabaseName
|
|
)
|
|
|
|
$Url = "TCP://${RemoteHostName}:${RemotePort}"
|
|
$AlterDb = "ALTER DATABASE $(ConvertTo-SQLName $DataBaseName) SET PARTNER = $(ConvertTo-SQLString $Url);
|
|
GO"
|
|
[void](Invoke-SQLText -SQL $AlterDb)
|
|
}
|
|
|
|
function New-SQLDatabase {
|
|
<#
|
|
.SYNOPSIS
|
|
Creates empty SQL database
|
|
|
|
.DESCRIPTION
|
|
Creates empty SQL database with default settings. Fails in case is the database already exists.
|
|
|
|
.PARAMETER DataBaseName
|
|
Database name.
|
|
|
|
.PARAMETER mdfFile
|
|
Name of the MDF (data) file. If not specified, the following value is used:
|
|
"C:\Program Files\Microsoft SQL Server\MSSQL11.MSSQLSERVER\MSSQL\DATA\{DataBasePathName}.mdf"
|
|
Where {DataBasePathName} is database name with all but A-Z, a-z, 0-9 characters
|
|
replaced by underscore.
|
|
|
|
.PARAMETER DataBaseName
|
|
Name of the LDF (transaction log) file. If not specified, the following value is used:
|
|
"C:\Program Files\Microsoft SQL Server\MSSQL11.MSSQLSERVER\MSSQL\DATA\{DataBasePathName}_log.mdf"
|
|
Where {DataBasePathName} is database name with all but A-Z, a-z, 0-9 characters
|
|
replaced by underscore.
|
|
#>
|
|
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[String]$DataBaseName,
|
|
[String]$mdfFile=$null,
|
|
[String]$ldfFile=$null
|
|
)
|
|
|
|
$DataBasePathName = $DataBaseName -replace '[^0-9a-zA-Z]', '_'
|
|
if (-not $mdfFile) {
|
|
$mdfFile = "C:\Program Files\Microsoft SQL Server\MSSQL11.MSSQLSERVER\MSSQL\DATA\${DataBasePathName}.mdf"
|
|
}
|
|
if (-not $ldfFile) {
|
|
$ldfFile = "C:\Program Files\Microsoft SQL Server\MSSQL11.MSSQLSERVER\MSSQL\DATA\${DataBasePathName}_log.ldf"
|
|
}
|
|
|
|
$NewDatabase = "
|
|
CREATE DATABASE $(ConvertTo-SQLName $DataBaseName)
|
|
CONTAINMENT = NONE
|
|
ON PRIMARY
|
|
( NAME = N$(ConvertTo-SQLString $DataBaseName), FILENAME = N$(ConvertTo-SQLString $mdfFile) , SIZE = 4096KB , FILEGROWTH = 1024KB )
|
|
LOG ON
|
|
( NAME = N$(ConvertTo-SQLString "${DataBaseName}_log"), FILENAME = N$(ConvertTo-SQLString $ldfFile) , SIZE = 1024KB , FILEGROWTH = 10%)
|
|
GO
|
|
USE $(ConvertTo-SQLName $DataBaseName)
|
|
GO
|
|
IF NOT EXISTS (SELECT name FROM sys.filegroups WHERE is_default=1 AND name = N'PRIMARY') ALTER DATABASE $(ConvertTo-SQLName $DataBaseName) MODIFY FILEGROUP [PRIMARY] DEFAULT
|
|
GO"
|
|
|
|
[void](Invoke-SQLText -SQL $NewDatabase)
|
|
}
|
|
|
|
function Initialize-SQLMirroringPrincipalStep1 {
|
|
<#
|
|
.SYNOPSIS
|
|
Prepares principal SQL Server for database mirroring (Stage 1)
|
|
|
|
.DESCRIPTION
|
|
Initializes mirroring endpoint (this is absolutely symmetric step to the mirror init). In addition to that it creates
|
|
a database and stores backups of it and its transaction log in the same directory as the endpoint certificate.
|
|
|
|
A firewall rule is created for endpoint if necessary.
|
|
|
|
.PARAMETER WorkDir
|
|
Workind directory. This directory should be tranferred to the mirror server after this
|
|
step is executed.
|
|
|
|
.PARAMETER DatabaseName
|
|
Mirrored database name. This name MUST be use at mirror server either.
|
|
|
|
#>
|
|
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[String]$WorkDir,
|
|
[parameter(Mandatory = $true)]
|
|
[String]$DataBaseName
|
|
)
|
|
|
|
[String]$EncryptionPassword = "$(New-Password 10)aA#3"
|
|
|
|
if (-not (Test-Path $WorkDir)) {
|
|
[void](New-Item -Type Directory $WorkDir)
|
|
}
|
|
$WorkDir = (Get-Item $WorkDir).FullName
|
|
if ((Get-ChildItem -Path $WorkDir).Length -gt 0) {
|
|
throw "Working directory $WorkDir is not empty"
|
|
}
|
|
|
|
$EndpointPort = Initialize-MirroringEndpoint $EncryptionPassword "$WorkDir\certificate.cer"
|
|
$EndpointPort | Set-Content "$WorkDir\endpoint-port.txt"
|
|
New-SQLDatabase $DataBaseName
|
|
|
|
$BackupDb = "BACKUP DATABASE $(ConvertTo-SQLName $DataBaseName) TO DISK = N$(ConvertTo-SQLString "$WorkDir\Source.bak") WITH NOFORMAT, INIT, NAME = N'Full Database Backup', SKIP, NOREWIND, NOUNLOAD, STATS = 10
|
|
GO"
|
|
[void](Invoke-SQLText -SQL $BackupDb)
|
|
$BackupLog = "BACKUP LOG $(ConvertTo-SQLName $DataBaseName) TO DISK = N$(ConvertTo-SQLString "$WorkDir\Source_log.bak") WITH NOFORMAT, INIT, NAME = N'Transaction Log Backup', SKIP, NOREWIND, NOUNLOAD, STATS = 10
|
|
GO"
|
|
[void](Invoke-SQLText -SQL $BackupLog)
|
|
}
|
|
|
|
function Initialize-SQLMirroringPrincipalStep2 {
|
|
<#
|
|
.SYNOPSIS
|
|
Prepares principal SQL Server for database mirroring (Stage 2)
|
|
|
|
.DESCRIPTION
|
|
Imports remote server certificate and grants it with access to the mirroring endpoint.
|
|
|
|
.PARAMETER RemoteHostName
|
|
Remote (mirror) host name. FQDN is preferred, but NetBIOS names and IP addresses are also accepted.
|
|
|
|
.PARAMETER RemoteWorkDir
|
|
Path to a copy of workdir obtained from mirror machine created on Stage 1.
|
|
#>
|
|
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[String]$RemoteHostName,
|
|
[parameter(Mandatory = $true)]
|
|
[String]$RemoteWorkDir
|
|
)
|
|
|
|
if (-not (Test-Path $RemoteWorkDir)) {
|
|
throw "Remote work dir '$RemoteWorkDir' was not found"
|
|
}
|
|
$RemoteWorkDir = (Get-Item $RemoteWorkDir).FullName
|
|
|
|
Complete-MirroringEndpoint $RemoteHostName $RemoteWorkDir
|
|
}
|
|
|
|
function Initialize-SQLMirroringPrincipalStep3 {
|
|
<#
|
|
.SYNOPSIS
|
|
Prepares principal SQL Server for database mirroring (Stage 3)
|
|
|
|
.DESCRIPTION
|
|
Completes mirror creation. This step must be globally the last one in mirror creation sequence.
|
|
|
|
Note that the remote host certificate is valid from the time it is created there. So
|
|
this step will fail if there is noticable different in time local and remote machines.
|
|
|
|
.PARAMETER RemoteHostName
|
|
Remote (principal) host name. FQDN is preferred, but NetBIOS names and IP addresses are also accepted.
|
|
|
|
.PARAMETER RemoteWorkDir
|
|
Path to a copy of workdir obtained from principal machine created on Stage 1.
|
|
|
|
.PARAMETER DatabaseName
|
|
Mirrored database name. This name MUST match principal database name and name provided on step 1.
|
|
#>
|
|
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[String]$RemoteHostName,
|
|
[parameter(Mandatory = $true)]
|
|
[String]$RemoteWorkDir,
|
|
[parameter(Mandatory = $true)]
|
|
[String]$DatabaseName
|
|
)
|
|
|
|
[int]$port = Get-Content "${RemoteWorkDir}\endpoint-port.txt"
|
|
Complete-SQLMirror $RemoteHostName $port $DatabaseName
|
|
}
|
|
|
|
function Initialize-SQLMirroringMirrorStep1 {
|
|
<#
|
|
.SYNOPSIS
|
|
Prepares mirror SQL Server for database mirroring (Stage1)
|
|
|
|
.DESCRIPTION
|
|
Initializes mirroring endpoint for mirror server. Stores mirroring endpoint certificate in Workdir.
|
|
|
|
.PARAMETER WorkDir
|
|
Workind directory. This directory should be tranferred to the principal server after this
|
|
step is executed.
|
|
|
|
.PARAMETER DatabaseName
|
|
Mirrored database name. This name MUST match principal database name.
|
|
|
|
#>
|
|
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[String]$WorkDir,
|
|
[parameter(Mandatory = $true)]
|
|
[String]$DatabaseName
|
|
)
|
|
|
|
[String]$EncryptionPassword = "$(New-Password 10)aA#3"
|
|
|
|
if (-not (Test-Path $WorkDir)) {
|
|
[void](New-Item -Type Directory $WorkDir)
|
|
}
|
|
$WorkDir = (Get-Item $WorkDir).FullName
|
|
|
|
$EndpointPort = Initialize-MirroringEndpoint $EncryptionPassword "$WorkDir\certificate.cer"
|
|
$EndpointPort | Set-Content "$WorkDir\endpoint-port.txt"
|
|
}
|
|
|
|
function Initialize-SQLMirroringMirrorStep2 {
|
|
<#
|
|
.SYNOPSIS
|
|
Prepares mirror SQL Server for database mirroring (Stage 2)
|
|
|
|
.DESCRIPTION
|
|
Imports remote server certificate and grants it with access to the mirroring endpoint.
|
|
Restores database obtained from principal and leaves it in 'Restoring' state.
|
|
|
|
.PARAMETER RemoteHostName
|
|
Remote (principal) host name. FQDN is preferred, but NetBIOS names and IP addresses are also accepted.
|
|
|
|
.PARAMETER RemoteWorkDir
|
|
Path to a copy of workdir obtained from principal machine created on Stage 1.
|
|
|
|
.PARAMETER DatabaseName
|
|
Mirrored database name. This name MUST match principal database name.
|
|
|
|
#>
|
|
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[String]$RemoteHostName,
|
|
[parameter(Mandatory = $true)]
|
|
[String]$RemoteWorkDir,
|
|
[parameter(Mandatory = $true)]
|
|
[String]$DataBaseName
|
|
)
|
|
|
|
if (-not (Test-Path $RemoteWorkDir)) {
|
|
throw "Remote work dir '$RemoteWorkDir' was not found"
|
|
}
|
|
$RemoteWorkDir = (Get-Item $RemoteWorkDir).FullName
|
|
|
|
Complete-MirroringEndpoint $RemoteHostName $RemoteWorkDir
|
|
|
|
$RestoreDb = "RESTORE DATABASE $(ConvertTo-SQLName $DataBaseName) FROM DISK = N$(ConvertTo-SQLString "$RemoteWorkDir\Source.bak") WITH FILE = 1, NORECOVERY, NOUNLOAD, REPLACE, STATS = 5
|
|
GO"
|
|
[void](Invoke-SQLText -SQL $RestoreDb)
|
|
$RestoreLog = "RESTORE LOG $(ConvertTo-SQLName $DataBaseName) FROM DISK = N$(ConvertTo-SQLString "$RemoteWorkDir\Source_log.bak") WITH FILE = 1, NORECOVERY, NOUNLOAD, STATS = 10
|
|
GO"
|
|
[void](Invoke-SQLText -SQL $RestoreLog)
|
|
}
|
|
|
|
function Initialize-SQLMirroringMirrorStep3 {
|
|
<#
|
|
.SYNOPSIS
|
|
Prepares mirror SQL Server for database mirroring (Stage 3)
|
|
|
|
.DESCRIPTION
|
|
Completes mirror creation. This step must be executed strictly before symmetric step on the principal.
|
|
|
|
Note that the remote host certificate is valid from the time it is created there. So
|
|
this step will fail if there is noticable different in time local and remote machines.
|
|
|
|
.PARAMETER RemoteHostName
|
|
Remote (principal) host name. FQDN is preferred, but NetBIOS names and IP addresses are also accepted.
|
|
|
|
.PARAMETER RemoteWorkDir
|
|
Path to a copy of workdir obtained from principal machine created on Stage 1.
|
|
|
|
.PARAMETER DatabaseName
|
|
Mirrored database name. This name MUST match principal database name.
|
|
|
|
#>
|
|
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[String]$RemoteHostName,
|
|
[parameter(Mandatory = $true)]
|
|
[String]$RemoteWorkDir,
|
|
[parameter(Mandatory = $true)]
|
|
[String]$DatabaseName
|
|
)
|
|
|
|
[int]$port = Get-Content "${RemoteWorkDir}\endpoint-port.txt"
|
|
Complete-SQLMirror $RemoteHostName $port $DatabaseName
|
|
}
|
|
|
|
function Get-NextFreePort {
|
|
<#
|
|
.SYNOPSIS
|
|
Returns specified desired port or closest next one unoccupied.
|
|
|
|
.PARAMETER Port
|
|
Desired port number.
|
|
|
|
#>
|
|
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[int]$Port
|
|
)
|
|
$OpenPorts = netstat -aon | select-string 'LISTENING' | Foreach-Object { (($_ -replace '^\s*', '' -split '\s+')[1] -split '.*:')[1] } | Sort-Object | Get-Unique
|
|
while ($OpenPorts.Contains(${Port})) {
|
|
$Port = $Port + 1
|
|
}
|
|
return $Port
|
|
}
|
|
|
|
function Initialize-AlwaysOn {
|
|
<#
|
|
.SYNOPSIS
|
|
Initializes AlwaysOn clustering on local SQL server and creates AlwaysOn endpoint listener. Returns AlwaysOn endpoint port number.
|
|
|
|
.DESCRIPTION
|
|
Enables AlwaysOn clustering on local SQL server. Creates AlwaysOn TCP endpoint on port 5022 or greater if the one is occupied.
|
|
#>
|
|
|
|
if (!(Test-Path SQLSERVER:\)) {
|
|
Import-Module sqlps
|
|
}
|
|
$MachineName = (Get-ChildItem SQLSERVER:\SQL)[0].PSChildName
|
|
$InstanceName = (Get-ChildItem SQLSERVER:\SQL\$MachineName).PSChildName
|
|
$AlwaysOnEnabled = ((Get-Item SQLSERVER:\SQL\$MachineName\$InstanceName) | select IsHadrEnabled).IsHadrEnabled
|
|
if (-not $AlwaysOnEnabled) {
|
|
Enable-SqlAlwaysOn -Path "SQLSERVER:\SQL\$MachineName\$InstanceName" -Force
|
|
}
|
|
$Instance = Get-Item SQLSERVER:\SQL\$MachineName\$InstanceName
|
|
$endpoint = $Instance.Endpoints["AlwaysOnEndpoint"]
|
|
if (-not $endpoint) {
|
|
$Port = Get-NextFreePort 5022
|
|
$endpoint = New-SqlHadrEndpoint AlwaysOnEndpoint -Port $Port -Path SQLSERVER:\SQL\$MachineName\$InstanceName
|
|
} else {
|
|
$Port = $endpoint.Protocol.Tcp.ListenerPort
|
|
}
|
|
if ($endpoint.EndpointState -ne "Started") {
|
|
$endpoint.Start()
|
|
}
|
|
return $Port
|
|
}
|
|
|
|
function New-AlwaysOnAvailabilityGroup {
|
|
<#
|
|
.SYNOPSIS
|
|
Creates new AlwaysOn availability group on primary replica.
|
|
|
|
.DESCRIPTION
|
|
Creates new AlwaysOn availability group on primary replica.
|
|
|
|
.PARAMETER WorkDir
|
|
Workind directory. This directory should be tranferred to the replica server(s) after this
|
|
step is executed.
|
|
|
|
.PARAMETER Name
|
|
Availability group name.
|
|
|
|
.PARAMETER DatabaseNames
|
|
Replica database(s) names.
|
|
|
|
.PARAMETER ReplicaDefs
|
|
Array of replica definition. Each definition is a hash table with replica-specific values.
|
|
|
|
Mandatory replica definition values are:
|
|
|
|
* [String] SERVER_INSTANCE - Replica server instance name
|
|
* [String] ENDPOINT_URL - Replica server endpoint URL. Normally it is TCP://fully.qualified.domain.name:5022
|
|
Port number should be obtained with Initialize-AlwaysOn at the replica server
|
|
* [String] AVAILABILITY_MODE - Replica availability mode. Can be "SYNCHRONOUS_COMMIT" or "ASYNCHRONOUS_COMMIT" only.
|
|
* [String] FAILOVER_MODE - Replica availability mode. Can be "MANUAL" or "AUTOMATIC" only.
|
|
|
|
Optional replica definition values are:
|
|
|
|
* [Integer] BACKUP_PRIORITY - Backup priority
|
|
* [Integer] SESSION_TIMEOUT - Session timeout
|
|
* [String] P_ALLOW_CONNECTIONS - Allowed connection types for "Primary" replica mode. Can be "READ_WRITE" or "ALL" only.
|
|
* [Array] P_READ_ONLY_ROUTING_LIST - List of replicas proviring readonly access when this one is primary.
|
|
* [String] S_ALLOW_CONNECTIONS - Allowed connection types for "Secondary" replica mode. Can be one of "NO", "READ_ONLY", "ALL".
|
|
* [String] S_READ_ONLY_ROUTING_URL - Replica read-only requests listener URL. Normally default server listener at port 1433 is used.
|
|
|
|
.PARAMETER Preferences
|
|
Hash table of general availability group preferences. All the keys are optional. Supported entry keys are:
|
|
|
|
* [String] AUTOMATED_BACKUP_PREFERENCE - Automated backup preference. Can be "PRIMARY", "SECONDARY_ONLY", "SECONDARY" or "NONE".
|
|
* [String] FAILURE_CONDITION_LEVEL - Failure condition level. Can be "1", "2", "3", "4" or "5".
|
|
* [Integer] HEALTH_CHECK_TIMEOUT - Replica health check timeout.
|
|
|
|
.PARAMETER ListenerDef
|
|
Hash table containing availability group listener configuration.
|
|
|
|
Mandatory listener configuration values are:
|
|
|
|
[String] NAME - Listener name.
|
|
|
|
Optional listener configuration values are:
|
|
|
|
[String] PORT - Listener port number. Integer value may be suffixed by a "+" symol (such as "5022+") which allows the routine to
|
|
select next free port with number greater or equal to the specified value.
|
|
[String] DHCP - DHCP listener address configuration flag. When any value specified, DHCP is used to configure listener
|
|
(this is also the default behavior). Also, a specific interface for DHCP may be specified as IP_ADDRESS/MASK
|
|
(like "192.168.1.0/255.255.255.0") as a value of the parameter.
|
|
[Array] STATIC - Static IP addresses to listen. IP addresses may be IPv4 addresses in the "IP_ADDRESS/MASK" form or IPv6
|
|
addresses in standard IPv6 notation.
|
|
|
|
See http://msdn.microsoft.com/en-us/library/ff878399.aspx page for more details regarding all the supported options.
|
|
#>
|
|
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[String]$WorkDir,
|
|
[parameter(Mandatory = $true)]
|
|
[String]$Name,
|
|
[parameter(Mandatory = $true)]
|
|
[Array]$DatabaseNames,
|
|
[parameter(Mandatory = $true)]
|
|
[Array]$ReplicaDefs,
|
|
[parameter]
|
|
[Hashtable]$Preferences,
|
|
[parameter(Mandatory = $true)]
|
|
[Hashtable]$ListenerDef
|
|
)
|
|
|
|
if (-not (Test-Path $WorkDir)) {
|
|
[void](New-Item -Type Directory $WorkDir)
|
|
}
|
|
$WorkDir = (Get-Item $WorkDir).FullName
|
|
if ((Get-ChildItem -Path $WorkDir).Length -gt 0) {
|
|
throw "Working directory $WorkDir is not empty"
|
|
}
|
|
|
|
$QuotedDBNames = ($DatabaseNames | ForEach-Object { ConvertTo-SQLName $_ }) -join ", "
|
|
|
|
if ($Preferences -eq $null) {
|
|
$Preferences = @()
|
|
}
|
|
$Prefs = @()
|
|
foreach($Pref in $Preferences) {
|
|
if ($Pref.Key -eq $null) {
|
|
Continue
|
|
}
|
|
if ($Pref.Key -eq "AUTOMATED_BACKUP_PREFERENCE") {
|
|
$Prefs = $Prefs + (Validate-Option $Pref.Key, $Pref.Value, @("PRIMARY", "SECONDARY_ONLY", "SECONDARY", "NONE") | New-ReplicaOption -Name $Pref.Key)
|
|
} elseif ($Pref.Key -eq "FAILURE_CONDITION_LEVEL") {
|
|
$Prefs = $Prefs + (Validate-Option $Pref.Key, $Pref.Value, @("1", "2", "3", "4", "5") | New-ReplicaOption -Name $Pref.Key)
|
|
} elseif ($Pref.Key -eq "HEALTH_CHECK_TIMEOUT") {
|
|
$Prefs = $Prefs + (Validate-IntOption $Pref.Key, $Pref.Value | New-ReplicaOption -Name $Pref.Key)
|
|
} else {
|
|
throw "Unexpected peferences option: '$($Pref.Key)'"
|
|
}
|
|
}
|
|
|
|
$ReplicaDefinitionsArray = @()
|
|
for ($i = 0; $i -lt $ReplicaDefs.Length; $i++) {
|
|
$RDef = $ReplicaDefs[$i]
|
|
if ($RDef.GetType().Name -ne "Hashtable") {
|
|
throw "All elements of ReplicaDefs array should be Hashtables"
|
|
}
|
|
|
|
$ReplicaOpts = @()
|
|
|
|
# Mandatory options
|
|
$ReplicaName = Validate-DefinedOption "SERVER_INSTANCE" $RDef["SERVER_INSTANCE"]
|
|
$ReplicaOpts = $ReplicaOpts + (Validate-DefinedOption "ENDPOINT_URL" $RDef["ENDPOINT_URL"] | ConvertTo-SQLString | New-ReplicaOption -Name "ENDPOINT_URL")
|
|
$ReplicaOpts = $ReplicaOpts + (Validate-Option "AVAILABILITY_MODE" $RDef["AVAILABILITY_MODE"] @("SYNCHRONOUS_COMMIT", "ASYNCHRONOUS_COMMIT") | New-ReplicaOption -Name "AVAILABILITY_MODE")
|
|
$ReplicaOpts = $ReplicaOpts + (Validate-Option "FAILOVER_MODE" $RDef["FAILOVER_MODE"] @("AUTOMATIC", "MANUAL") | New-ReplicaOption -Name "FAILOVER_MODE")
|
|
|
|
# Optional options
|
|
if ($RDef["BACKUP_PRIORITY"] -ne $null) {
|
|
$ReplicaOpts = $ReplicaOpts + (Validate-IntOption "BACKUP_PRIORITY" $RDef["BACKUP_PRIORITY"] | New-ReplicaOption -Name "BACKUP_PRIORITY")
|
|
}
|
|
if ($RDef["SESSION_TIMEOUT"] -ne $null) {
|
|
$ReplicaOpts = $ReplicaOpts + (Validate-IntOption "SESSION_TIMEOUT" $RDef["SESSION_TIMEOUT"] | New-ReplicaOption -Name "SESSION_TIMEOUT")
|
|
}
|
|
|
|
$SecondaryRole = @()
|
|
if ($RDef["S_ALLOW_CONNECTIONS"] -ne $null) {
|
|
$SecondaryRole = $SecondaryRole + (Validate-Option "S_ALLOW_CONNECTIONS" $RDef["S_ALLOW_CONNECTIONS"] @("NO", "READ_ONLY", "ALL") | New-ReplicaOption -Name "ALLOW_CONNECTIONS")
|
|
}
|
|
if ($RDef["S_READ_ONLY_ROUTING_URL"] -ne $null) {
|
|
$SecondaryRole = $SecondaryRole + ($RDef["S_READ_ONLY_ROUTING_URL"] | ConvertTo-SQLString | New-ReplicaOption -Name "ALLOW_CONNECTIONS")
|
|
}
|
|
if ($SecondaryRole.Length -gt 0) {
|
|
$ReplicaOpts = $ReplicaOpts + ("( $($SecondaryRole -join ', ') )" | New-ReplicaOption -Name "SECONDARY_ROLE")
|
|
}
|
|
|
|
$PrimaryRole = @()
|
|
if ($RDef["P_ALLOW_CONNECTIONS"] -ne $null) {
|
|
$PrimaryRole = $PrimaryRole + (Validate-Option "P_ALLOW_CONNECTIONS" $RDef["P_ALLOW_CONNECTIONS"] @("READ_WRITE", "ALL") | New-ReplicaOption -Name "ALLOW_CONNECTIONS")
|
|
}
|
|
if ($RDef["P_READ_ONLY_ROUTING_LIST"] -ne $null) {
|
|
$PrimaryRole = $PrimaryRole + ((($RDef["P_READ_ONLY_ROUTING_LIST"] | ForEach-Object { ConvertTo-SQLString $_ }) -join ', ') | New-ReplicaOption -Name "ALLOW_CONNECTIONS")
|
|
}
|
|
if ($PrimaryRole.Length -gt 0) {
|
|
$ReplicaOpts = $ReplicaOpts + ("( $($PrimaryRole -join ', ') )" | New-ReplicaOption -Name "PRIMARY_ROLE")
|
|
}
|
|
|
|
$ReplicaDefinitionsArray = $ReplicaDefinitionsArray +
|
|
# TCP://bravo.murano.local:5022
|
|
"N$(ConvertTo-SQLString $ReplicaName) WITH ($($ReplicaOpts -join ', '))"
|
|
}
|
|
$ReplicaDefinitions = $ReplicaDefinitionsArray -join ",`r`n ";
|
|
|
|
if ($ListenerDef["DHCP"] -ne $null) {
|
|
if ($ListenerDef["DHCP"].matches("\d+\.\d+\.\d+\.\d+/\d+\.\d+\.\d+\.\d+")) {
|
|
($IpAddr, $Mask) = $ListenerDef["DHCP"] -split "/"
|
|
$ListenerAddr = "DHCP ON ( $IpAddr, $Mask )"
|
|
} else {
|
|
$ListenerAddr = "DHCP"
|
|
}
|
|
} else {
|
|
[array]$IPAddresses = $ListenerDef["STATIC"]
|
|
if (($IPAddresses -eq $null) -or ($IPAddresses.Count -eq 0)) {
|
|
$ListenerAddr = "DHCP"
|
|
} else {
|
|
$ConvertedOpts = @()
|
|
foreach ($IpOption in $IPAddresses) {
|
|
# IPv4
|
|
if ($IpOption -match "\d+\.\d+\.\d+\.\d+/\d+\.\d+\.\d+\.\d+") {
|
|
($IpAddr, $Mask) = $IpOption -split "/"
|
|
$ConvertedOpts = $ConvertedOpts + "( $(ConvertTo-SQLString $IpAddr), $(ConvertTo-SQLString $Mask) )"
|
|
continue
|
|
}
|
|
# IPv6
|
|
if ($IpOption -match "^(((?=(?>.*?::)(?!.*::)))(::)?([0-9A-F]{1,4}::?){0,5}|([0-9A-F]{1,4}:){6})(\2([0-9A-F]{1,4}(::?|$)){0,2}|((25[0-5]|(2[0-4]|1\d|[1-9])?\d)(\.|$)){4}|[0-9A-F]{1,4}:[0-9A-F]{1,4})(?<![^:]:|\.)\z") {
|
|
$ConvertedOpts = $ConvertedOpts + "( $(ConvertTo-SQLString $IpOption) )"
|
|
continue
|
|
}
|
|
throw "Malformed IPv4/IPv6 address: $IpOption"
|
|
}
|
|
$ListenerAddr = "IP ( $($ConvertedOpts -join ', ') )"
|
|
}
|
|
}
|
|
if (($ListenerDef["NAME"] -eq $null) -or ($ListenerDef["NAME"] -match "^\s*$")) {
|
|
throw "Listener name is required"
|
|
}
|
|
if (-not ($ListenerDef["NAME"] -match "^[A-Za-z0-9\._\-]+$")) {
|
|
throw "Illegal listener name. It can contain only alphanumeric characters, dashes (-), and hyphens (_), in any order."
|
|
}
|
|
$Port = $null
|
|
if ($ListenerDef["PORT"] -ne $null) {
|
|
if ($ListenerDef["PORT"] -match "\d+\+") {
|
|
$StartingPort = $ListenerDef["PORT"] -replace "\+", ""
|
|
$Port = Get-NextFreePort $StartingPort
|
|
$ListenerAddr = $ListenerAddr + ", PORT = $Port"
|
|
} else {
|
|
if ($ListenerDef["PORT"] -match "\d+") {
|
|
$ListenerAddr = $ListenerAddr + ", PORT = $($ListenerDef["PORT"])"
|
|
} else {
|
|
throw "Invalid port value: $($ListenerDef["PORT"])"
|
|
}
|
|
}
|
|
}
|
|
$Listener = "LISTENER '$($ListenerDef["NAME"])' ( WITH $ListenerAddr )"
|
|
|
|
$Name | Out-File "$WorkDir\avgroup.name"
|
|
|
|
for ($i = 0; $i -lt $DatabaseNames.Length; $i++) {
|
|
$DataBaseName = $DatabaseNames[$i]
|
|
$DataBaseName | Out-File "$WorkDir\db$i.name"
|
|
New-SQLDatabase $DataBaseName
|
|
$BackupDb = "BACKUP DATABASE $(ConvertTo-SQLName $DataBaseName) TO DISK = N$(ConvertTo-SQLString "$WorkDir\db$i.bak") WITH NOFORMAT, INIT, NAME = N'Full Database Backup', SKIP, NOREWIND, NOUNLOAD, STATS = 10
|
|
GO"
|
|
[void](Invoke-SQLText -SQL $BackupDb)
|
|
$BackupLog = "BACKUP LOG $(ConvertTo-SQLName $DataBaseName) TO DISK = N$(ConvertTo-SQLString "$WorkDir\db${i}.log.bak") WITH NOFORMAT, INIT, NAME = N'Transaction Log Backup', SKIP, NOREWIND, NOUNLOAD, STATS = 10
|
|
GO"
|
|
[void](Invoke-SQLText -SQL $BackupLog)
|
|
}
|
|
$ReplicaDefinitionsArray = @()
|
|
if ($Prefs.Length -gt 0) {
|
|
$PrefsLine = "WITH ( $($Prefs -join ', ') )"
|
|
} else {
|
|
$PrefsLine = ""
|
|
}
|
|
$SQL = "CREATE AVAILABILITY GROUP $(ConvertTo-SQLName $Name) $PrefsLine
|
|
FOR DATABASE $QuotedDBNames
|
|
REPLICA ON`r`n $ReplicaDefinitions
|
|
$Listener;
|
|
"
|
|
[void](Invoke-SQLText -SQL $SQL)
|
|
return $Port
|
|
}
|
|
|
|
function New-AlwaysOnAvailabilityGroupReplica {
|
|
<#
|
|
.SYNOPSIS
|
|
Creates AlwaysOn availability group secondary replica
|
|
|
|
.DESCRIPTION
|
|
Creates AlwaysOn availability group secondary replica based on information provided to and by New-AlwaysOnAvailabilityGroup.
|
|
|
|
.PARAMETER WorkDir
|
|
Working directory which was transferred from the primary replica.
|
|
#>
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[String]$WorkDir
|
|
)
|
|
if (-not (Test-Path $WorkDir)) {
|
|
throw "Work dir '$WorkDir' not found"
|
|
}
|
|
$WorkDirObj = Get-Item -Path $WorkDir
|
|
$WorkDir = $WorkDirObj.FullName
|
|
$GroupName = Get-Content $WorkDirObj.GetFiles("avgroup.name").FullName
|
|
|
|
$JoinGroup = "ALTER AVAILABILITY GROUP $(ConvertTo-SQLName $GroupName) JOIN
|
|
GO"
|
|
[void](Invoke-SQLText -SQL $JoinGroup)
|
|
|
|
for ($i = 0; ; $i++) {
|
|
$File = $WorkDirObj.GetFiles("db$i.name")
|
|
if (-not $File) {
|
|
break;
|
|
}
|
|
$DataBaseName = Get-Content $WorkDirObj.GetFiles("db$i.name").FullName
|
|
$RestoreDb = "RESTORE DATABASE $(ConvertTo-SQLName $DataBaseName) FROM DISK = N$(ConvertTo-SQLString "$WorkDir\db$i.bak") WITH FILE = 1, NORECOVERY, NOUNLOAD, REPLACE, STATS = 5
|
|
GO"
|
|
[void](Invoke-SQLText -SQL $RestoreDb)
|
|
$RestoreLog = "RESTORE LOG $(ConvertTo-SQLName $DataBaseName) FROM DISK = N$(ConvertTo-SQLString "$WorkDir\db$i.log.bak") WITH FILE = 1, NORECOVERY, NOUNLOAD, STATS = 10
|
|
GO"
|
|
[void](Invoke-SQLText -SQL $RestoreLog)
|
|
$AlterDB = "ALTER DATABASE $(ConvertTo-SQLName $DataBaseName) SET HADR AVAILABILITY GROUP = $(ConvertTo-SQLName $GroupName)
|
|
GO"
|
|
[void](Invoke-SQLText -SQL $AlterDB)
|
|
}
|
|
}
|
|
|
|
function New-ReplicaOption {
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[String]$Name,
|
|
[parameter(Mandatory = $true, ValueFromPipeline = $true)]
|
|
[String]$Value
|
|
)
|
|
return "$Name = $Value"
|
|
}
|
|
|
|
function Validate-Option {
|
|
<#
|
|
.SYNOPSIS
|
|
Checks that the value is one of allowed values
|
|
|
|
.DESCRIPTION
|
|
Checks that the value is one of allowed values or throws exception otherwise. Returns provided value.
|
|
|
|
.PARAMETER Name
|
|
Option name. Used only for error message.
|
|
|
|
.PARAMETER Value
|
|
Option value.
|
|
|
|
.PARAMETER Allowed
|
|
List of allowed option valus.
|
|
#>
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[String]$Name,
|
|
[String]$Value,
|
|
[Array]$Allowed
|
|
)
|
|
if (($Value -eq $null) -or ($Value -eq "")) {
|
|
throw "No value was provided for $Name"
|
|
}
|
|
foreach ($V in $Allowed) {
|
|
if ($V -eq $Value) {
|
|
return $Value
|
|
}
|
|
}
|
|
throw "Provided value '$Value' for $Name is not one of $($Allowed -join ', ')"
|
|
}
|
|
|
|
function Validate-IntOption {
|
|
<#
|
|
.SYNOPSIS
|
|
Checks that the value is integer
|
|
|
|
.DESCRIPTION
|
|
Checks that the value is integer. Returns provided value.
|
|
|
|
.PARAMETER Name
|
|
Option name. Used only for error message.
|
|
|
|
.PARAMETER Value
|
|
Option value.
|
|
#>
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[String]$Name,
|
|
[parameter]
|
|
[String]$Value
|
|
)
|
|
if (($Value -eq $null) -or ($Value -eq "")) {
|
|
throw "No value was provided for $Name"
|
|
}
|
|
if (-not ("$Value" -match "^[+-]?\d+$")) {
|
|
throw "Provided value '$Value' for $Name is not a number"
|
|
}
|
|
return $Value
|
|
}
|
|
|
|
function Validate-DefinedOption {
|
|
<#
|
|
.SYNOPSIS
|
|
Checks that the value is not null
|
|
|
|
.DESCRIPTION
|
|
Checks that the value is not null. Returns provided value.
|
|
|
|
.PARAMETER Name
|
|
Option name. Used only for error message.
|
|
|
|
.PARAMETER Value
|
|
Option value.
|
|
#>
|
|
param(
|
|
[parameter(Mandatory = $true)]
|
|
[String]$Name,
|
|
[parameter(Mandatory = $false)]
|
|
[String]$Value
|
|
)
|
|
if (($Value -eq $null) -or ($Value -eq "")) {
|
|
throw "No value was provided for $Name"
|
|
}
|
|
return $Value
|
|
}
|
|
|
|
|
|
|