
separately from templates in the ./scripts/ directory. test_windows_agent.py updated. Formatting in windows_agent.py fixed. Tracing and error capturing added to Set-LocalUserPassword. Function Set-LocalUserPassword removed as it is implemented by workflow means. Scripts now stored in one common folder. Scripts updated from murano-deployment. Templates updated to reflect new script names. Workflow template can reference multiple script files. These script files will be concatinated and encrypted with base64. Change-Id: Icb3532d2fb724bbb711c06086cb906bde22a380f Fixed flake8 reported issues. Change-Id: Icb3532d2fb724bbb711c06086cb906bde22a380f Fixed test failure after reformatting. Change-Id: Icb3532d2fb724bbb711c06086cb906bde22a380f Fixed issues with test. Revert back path selection for scripts folder. It is reasonable to have an ability to use own folder for custom workflows so script path is relative to template location. Change-Id: Icb3532d2fb724bbb711c06086cb906bde22a380f
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
|
|
}
|
|
|
|
|
|
|