├── AlwaysOn ├── 00_setup_cluster.ps1 ├── 01_setup_instances.ps1 ├── 01_setup_instances_SQL2016.ps1 ├── 01_setup_instances_SQL2017.ps1 ├── 02_setup_availability_group.ps1 ├── 02_setup_availability_group_SQL2016.ps1 ├── 02_setup_availability_group_SQL2017.ps1 ├── 03_add_SQL03_to_Cluster_and_AvailabilityGroup.ps1 ├── 99_remove.ps1 └── README.md ├── AlwaysOn_DatabaseOwner └── 01_database_owner.ps1 ├── AlwaysOn_MultiInstance ├── 00_setup_cluster.ps1 ├── 01_setup_instances.ps1 ├── 02_setup_availability_group.ps1 ├── 02_setup_availability_group_demo_01_AgHadr_Endpoint_XESession.ps1 ├── 02_setup_availability_group_demo_02_empty_availability_group.ps1 ├── 02_setup_availability_group_demo_03a_add_database_with_manual_seeding.ps1 ├── 02_setup_availability_group_demo_03b_add_database_with_automatic_seeding.ps1 └── 03_add_SRV3_to_Cluster_and_AvailabilityGroup.ps1 ├── AutomatedLab ├── 01_Install_HyperV.ps1 ├── 02_Install_AutomatedLab.ps1 ├── 03_Setup_TestLab.ps1 ├── 04_Download_CustomAssets.ps1 ├── CustomAssets │ ├── DockerImages │ │ └── README.md │ ├── FileServerISOs │ │ └── README.md │ ├── GPOs │ │ ├── GPO_COMP_CredSSP_Server │ │ │ ├── manifest.xml │ │ │ └── {A30D2B62-F106-40AD-9C6F-6E410751F850} │ │ │ │ ├── Backup.xml │ │ │ │ ├── DomainSysvol │ │ │ │ └── GPO │ │ │ │ │ └── Machine │ │ │ │ │ ├── comment.cmtx │ │ │ │ │ └── registry.pol │ │ │ │ ├── bkupInfo.xml │ │ │ │ └── gpreport.xml │ │ ├── GPO_COMP_PasswordAge_0 │ │ │ ├── manifest.xml │ │ │ └── {9900D0B7-C5F0-414E-80D4-9BF07C4CC8C0} │ │ │ │ ├── Backup.xml │ │ │ │ ├── DomainSysvol │ │ │ │ └── GPO │ │ │ │ │ └── Machine │ │ │ │ │ └── microsoft │ │ │ │ │ └── windows nt │ │ │ │ │ └── SecEdit │ │ │ │ │ └── GptTmpl.inf │ │ │ │ ├── bkupInfo.xml │ │ │ │ └── gpreport.xml │ │ ├── GPO_COMP_PowerPlan_HighPerformance │ │ │ ├── manifest.xml │ │ │ └── {B69913CD-5BFA-4CCA-8A5E-258B50F4E5A8} │ │ │ │ ├── Backup.xml │ │ │ │ ├── DomainSysvol │ │ │ │ └── GPO │ │ │ │ │ └── Machine │ │ │ │ │ ├── comment.cmtx │ │ │ │ │ └── registry.pol │ │ │ │ ├── bkupInfo.xml │ │ │ │ └── gpreport.xml │ │ ├── GPO_USR_DriveMapping_F_FileServer │ │ │ ├── manifest.xml │ │ │ └── {5CB66590-4C6F-4196-AC68-C9F028B5A7CB} │ │ │ │ ├── Backup.xml │ │ │ │ ├── DomainSysvol │ │ │ │ └── GPO │ │ │ │ │ └── User │ │ │ │ │ └── Preferences │ │ │ │ │ └── Drives │ │ │ │ │ └── Drives.xml │ │ │ │ ├── bkupInfo.xml │ │ │ │ └── gpreport.xml │ │ └── GPO_USR_Explorer │ │ │ ├── manifest.xml │ │ │ └── {DC1CA3A6-3D05-42C2-9A55-EE7C8ECE1919} │ │ │ ├── Backup.xml │ │ │ ├── DomainSysvol │ │ │ └── GPO │ │ │ │ └── User │ │ │ │ └── Preferences │ │ │ │ └── Registry │ │ │ │ └── Registry.xml │ │ │ ├── bkupInfo.xml │ │ │ └── gpreport.xml │ ├── SampleDatabases │ │ └── README.md │ └── Software │ │ └── README.md ├── CustomScripts │ ├── Docker_Databases.ps1 │ ├── README.md │ ├── SQLServer_AlwaysOn.ps1 │ ├── Win2022_Oracle.ps1 │ └── Win2022_SQLServer.ps1 └── README.md ├── LICENSE ├── PowerShell └── ConvertTo-Json.ipynb ├── README.md ├── SQLServer ├── Fragmentation.sql └── Fragmentation_createDemoData.sql ├── dbatools ├── Get-CU.ps1 ├── Test_Experimental_Connect-DbaInstance.ps1 └── downgrade_database.ps1 └── sp_Blitz_markdown ├── README.md └── sp_Blitz_output_as_markdown.ps1 /AlwaysOn/00_setup_cluster.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [string]$DomainName = 'ORDIX', 4 | [string]$DomainController = 'DC', 5 | [string[]]$ClusterNodes = @('SQL01', 'SQL02'), 6 | [string]$ClusterName = 'CLUSTER1', 7 | [string]$ClusterIP = '192.168.3.70' 8 | ) 9 | 10 | $ErrorActionPreference = 'Stop' 11 | 12 | Import-Module -Name PSFramework 13 | 14 | try { 15 | 16 | Write-PSFMessage -Level Host -Message 'Install cluster feature on each node' 17 | Invoke-Command -ComputerName $ClusterNodes -ScriptBlock { Install-WindowsFeature -Name Failover-Clustering -IncludeManagementTools } | Format-Table 18 | 19 | Write-PSFMessage -Level Host -Message 'Install cluster admin tools on local computer' 20 | Install-WindowsFeature -Name RSAT-Clustering | Format-Table 21 | 22 | Write-PSFMessage -Level Host -Message 'Run cluster test' 23 | $clusterTest = Test-Cluster -Node $ClusterNodes 24 | Write-PSFMessage -Level Host -Message "Result of cluster test is available at: $($clusterTest.FullName)" 25 | <# To display result in web browser run: 26 | & $clusterTest.FullName 27 | #> 28 | 29 | Write-PSFMessage -Level Host -Message 'Create the cluster' 30 | $null = New-Cluster -Name $ClusterName -Node $ClusterNodes -StaticAddress $ClusterIP 31 | 32 | Write-PSFMessage -Level Host -Message 'Create a share as cluster quorum and configure the cluster' 33 | Invoke-Command -ComputerName $DomainController -ScriptBlock { 34 | $null = New-Item -Path "C:\WindowsClusterQuorum_$using:ClusterName" -ItemType Directory 35 | $null = New-SmbShare -Path "C:\WindowsClusterQuorum_$using:ClusterName" -Name "WindowsClusterQuorum_$using:ClusterName" 36 | $null = Grant-SmbShareAccess -Name "WindowsClusterQuorum_$using:ClusterName" -AccountName "$using:DomainName\$using:ClusterName$" -AccessRight Full -Force 37 | } 38 | Set-ClusterQuorum -Cluster $ClusterName -NodeAndFileShareMajority "\\$DomainController\WindowsClusterQuorum_$ClusterName" | Format-List 39 | 40 | Write-PSFMessage -Level Host -Message 'Grant necessary rights to the computer account of the cluster' 41 | $adComputerGUID = [GUID]::new('bf967a86-0de6-11d0-a285-00aa003049e2') 42 | $adClusterComputer = Get-ADComputer -Filter "Name -eq '$ClusterName'" 43 | $adClusterIdentity = [System.Security.Principal.SecurityIdentifier]::new($adClusterComputer.SID) 44 | $adClusterOU = [ADSI]([ADSI]"LDAP://$($adClusterComputer.DistinguishedName)").Parent 45 | $accessRule1 = [System.DirectoryServices.ActiveDirectoryAccessRule]::new($adClusterIdentity, "ReadProperty", "Allow", [GUID]::Empty, "All", [GUID]::Empty) 46 | $accessRule2 = [System.DirectoryServices.ActiveDirectoryAccessRule]::new($adClusterIdentity, "CreateChild", "Allow", $adComputerGUID, "All", [GUID]::Empty) 47 | $adClusterOU.psbase.ObjectSecurity.AddAccessRule($accessRule1) 48 | $adClusterOU.psbase.ObjectSecurity.AddAccessRule($accessRule2) 49 | $adClusterOU.psbase.CommitChanges() 50 | 51 | Write-PSFMessage -Level Host -Message 'finished' 52 | 53 | } catch { Write-PSFMessage -Level Warning -Message 'failed' -ErrorRecord $_ } 54 | -------------------------------------------------------------------------------- /AlwaysOn/01_setup_instances.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [string]$DomainName = 'ORDIX', 4 | [string]$DomainController = 'DC', 5 | [string[]]$ClusterNodes = @('SQL01', 'SQL02'), 6 | [string[]]$SqlInstances = @('SQL01', 'SQL02'), 7 | [string]$SQLServerServiceAccount = 'gMSA-SQLServer', 8 | [SecureString]$AdminPassword = (ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force), 9 | [string]$SQLServerSourcesPath = '\\fs\Software\SQLServer\ISO', 10 | [string]$SQLServerPatchesPath = '\\fs\Software\SQLServer\CU', 11 | [string]$SampleDatabasesPath = '\\fs\SampleDatabases', 12 | [string]$DatabaseName = 'AdventureWorks' 13 | ) 14 | 15 | $ErrorActionPreference = 'Stop' 16 | 17 | Import-Module -Name PSFramework 18 | Import-Module -Name dbatools 19 | 20 | $PSDefaultParameterValues['*-Dba*:EnableException'] = $true 21 | $PSDefaultParameterValues['*-Dba*:Confirm'] = $false 22 | 23 | try { 24 | 25 | $administratorCredential = [PSCredential]::new("$DomainName\Admin", $AdminPassword) 26 | $sqlServerCredential = [PSCredential]::new("$DomainName\$SQLServerServiceAccount$", [SecureString]::new()) 27 | 28 | if ($null -eq (Get-ADServiceAccount -Filter "Name -eq '$SQLServerServiceAccount'") ) { 29 | throw "Service Account '$SQLServerServiceAccount' for SQL Server not found. Creation of this account is not part of this script but done in https://github.com/andreasjordan/demos/blob/master/AutomatedLab/CustomScripts/SQLServer_AlwaysOn.ps1" 30 | } 31 | 32 | Write-PSFMessage -Level Host -Message 'Change powerplan of cluster nodes to high performance' 33 | Set-DbaPowerPlan -ComputerName $ClusterNodes | Format-Table 34 | 35 | Write-PSFMessage -Level Host -Message 'Install SQL Server instances on cluster nodes' 36 | $installParams = @{ 37 | SqlInstance = $SqlInstances 38 | Version = 2022 39 | Feature = 'Engine' 40 | Path = $SQLServerSourcesPath 41 | UpdateSourcePath = $SQLServerPatchesPath 42 | EngineCredential = $sqlServerCredential 43 | AgentCredential = $sqlServerCredential 44 | AuthenticationMode = 'Mixed' 45 | Credential = $administratorCredential 46 | Restart = $true 47 | EnableException = $false 48 | } 49 | $installResult = @( ) 50 | $installResult += Install-DbaInstance @installParams 51 | $installResult | Format-Table 52 | if ($installResult.Count -lt $SqlInstances.Count -or $false -in $installResult.Successful) { 53 | throw "Install-DbaInstance not successful" 54 | } 55 | 56 | Write-PSFMessage -Level Host -Message 'Grant instant file initialization rights to SQL Server service account on cluster nodes' 57 | Set-DbaPrivilege -ComputerName $ClusterNodes -Type IFI 58 | 59 | Write-PSFMessage -Level Host -Message 'Add rule to firewall on cluster nodes' 60 | New-DbaFirewallRule -SqlInstance $SqlInstances | Format-Table 61 | 62 | Write-PSFMessage -Level Host -Message 'Configure SQL Server instances: MaxMemory / MaxDop / CostThresholdForParallelism' 63 | Set-DbaMaxMemory -SqlInstance $SqlInstances -Max 2048 | Format-Table 64 | Set-DbaMaxDop -SqlInstance $SqlInstances | Format-Table 65 | Set-DbaSpConfigure -SqlInstance $SqlInstances -Name CostThresholdForParallelism -Value 50 | Format-Table 66 | 67 | Write-PSFMessage -Level Host -Message 'Restore and configure demo database' 68 | $null = Restore-DbaDatabase -SqlInstance $SqlInstances[0] -Path "$SampleDatabasesPath\AdventureWorks2017.bak" -DatabaseName $DatabaseName 69 | $null = Set-DbaDbOwner -SqlInstance $SqlInstances[0] -Database $DatabaseName -TargetLogin sa 70 | $null = Set-DbaDbRecoveryModel -SqlInstance $SqlInstances[0] -Database $DatabaseName -RecoveryModel Full 71 | $null = Backup-DbaDatabase -SqlInstance $SqlInstances[0] -Database $DatabaseName 72 | 73 | Write-PSFMessage -Level Host -Message 'finished' 74 | 75 | } catch { Write-PSFMessage -Level Warning -Message 'failed' -ErrorRecord $_ } 76 | -------------------------------------------------------------------------------- /AlwaysOn/01_setup_instances_SQL2016.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [string]$DomainName = 'ORDIX', 4 | [string]$DomainController = 'DC', 5 | [string[]]$ClusterNodes = @('SQL01', 'SQL02'), 6 | [string[]]$SqlInstances = @('SQL01\SQL2016', 'SQL02\SQL2016'), 7 | [string]$SQLServerServiceAccount = 'gMSA-SQLServer', 8 | [SecureString]$AdminPassword = (ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force), 9 | [string]$SQLServerSourcesPath = '\\fs\Software\SQLServer\ISO', 10 | [string]$SQLServerPatchesPath = '\\fs\Software\SQLServer\CU', 11 | [string]$SampleDatabasesPath = '\\fs\SampleDatabases', 12 | [string]$DatabaseName = 'AdventureWorks' 13 | ) 14 | 15 | $ErrorActionPreference = 'Stop' 16 | 17 | Import-Module -Name PSFramework 18 | Import-Module -Name dbatools 19 | 20 | $PSDefaultParameterValues['*-Dba*:EnableException'] = $true 21 | $PSDefaultParameterValues['*-Dba*:Confirm'] = $false 22 | 23 | try { 24 | 25 | $administratorCredential = [PSCredential]::new("$DomainName\Admin", $AdminPassword) 26 | $sqlServerCredential = [PSCredential]::new("$DomainName\$SQLServerServiceAccount$", [SecureString]::new()) 27 | 28 | if ($null -eq (Get-ADServiceAccount -Filter "Name -eq '$SQLServerServiceAccount'") ) { 29 | throw "Service Account '$SQLServerServiceAccount' for SQL Server not found. Creation of this account is not part of this script but done in https://github.com/andreasjordan/demos/blob/master/AutomatedLab/CustomScripts/SQLServer_AlwaysOn.ps1" 30 | } 31 | 32 | Write-PSFMessage -Level Host -Message 'Change powerplan of cluster nodes to high performance' 33 | Set-DbaPowerPlan -ComputerName $ClusterNodes | Format-Table 34 | 35 | Write-PSFMessage -Level Host -Message 'Install SQL Server instances on cluster nodes' 36 | $installParams = @{ 37 | SqlInstance = $SqlInstances 38 | Version = 2016 39 | Feature = 'Engine' 40 | Path = $SQLServerSourcesPath 41 | UpdateSourcePath = $SQLServerPatchesPath 42 | EngineCredential = $sqlServerCredential 43 | AgentCredential = $sqlServerCredential 44 | AuthenticationMode = 'Mixed' 45 | Credential = $administratorCredential 46 | Restart = $true 47 | EnableException = $false 48 | } 49 | $installResult = Install-DbaInstance @installParams 50 | $installResult | Format-Table 51 | if ($false -in $installResult.Successful) { 52 | throw "Install-DbaInstance not successful" 53 | } 54 | 55 | Write-PSFMessage -Level Host -Message 'Grant instant file initialization rights to SQL Server service account on cluster nodes' 56 | Set-DbaPrivilege -ComputerName $ClusterNodes -Type IFI 57 | 58 | Write-PSFMessage -Level Host -Message 'Configure SQL Server instances: MaxMemory / MaxDop / CostThresholdForParallelism' 59 | Set-DbaMaxMemory -SqlInstance $SqlInstances -Max 2048 | Format-Table 60 | Set-DbaMaxDop -SqlInstance $SqlInstances | Format-Table 61 | Set-DbaSpConfigure -SqlInstance $SqlInstances -Name CostThresholdForParallelism -Value 50 | Format-Table 62 | 63 | Write-PSFMessage -Level Host -Message 'Restore and configure demo database' 64 | $null = Restore-DbaDatabase -SqlInstance $SqlInstances[0] -Path "$SampleDatabasesPath\AdventureWorks2016.bak" -DatabaseName $DatabaseName 65 | $null = Set-DbaDbOwner -SqlInstance $SqlInstances[0] -Database $DatabaseName -TargetLogin sa 66 | $null = Set-DbaDbRecoveryModel -SqlInstance $SqlInstances[0] -Database $DatabaseName -RecoveryModel Full 67 | $null = Backup-DbaDatabase -SqlInstance $SqlInstances[0] -Database $DatabaseName 68 | 69 | Write-PSFMessage -Level Host -Message 'finished' 70 | 71 | } catch { Write-PSFMessage -Level Warning -Message 'failed' -ErrorRecord $_ } 72 | -------------------------------------------------------------------------------- /AlwaysOn/01_setup_instances_SQL2017.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [string]$DomainName = 'ORDIX', 4 | [string]$DomainController = 'DC', 5 | [string[]]$ClusterNodes = @('SQL01', 'SQL02'), 6 | [string[]]$SqlInstances = @('SQL01\SQL2017', 'SQL02\SQL2017'), 7 | [string]$SQLServerServiceAccount = 'gMSA-SQLServer', 8 | [SecureString]$AdminPassword = (ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force), 9 | [string]$SQLServerSourcesPath = '\\fs\Software\SQLServer\ISO', 10 | [string]$SQLServerPatchesPath = '\\fs\Software\SQLServer\CU', 11 | [string]$SampleDatabasesPath = '\\fs\SampleDatabases', 12 | [string]$DatabaseName = 'AdventureWorks' 13 | ) 14 | 15 | $ErrorActionPreference = 'Stop' 16 | 17 | Import-Module -Name PSFramework 18 | Import-Module -Name dbatools 19 | 20 | $PSDefaultParameterValues['*-Dba*:EnableException'] = $true 21 | $PSDefaultParameterValues['*-Dba*:Confirm'] = $false 22 | 23 | try { 24 | 25 | $administratorCredential = [PSCredential]::new("$DomainName\Admin", $AdminPassword) 26 | $sqlServerCredential = [PSCredential]::new("$DomainName\$SQLServerServiceAccount$", [SecureString]::new()) 27 | 28 | if ($null -eq (Get-ADServiceAccount -Filter "Name -eq '$SQLServerServiceAccount'") ) { 29 | throw "Service Account '$SQLServerServiceAccount' for SQL Server not found. Creation of this account is not part of this script but done in https://github.com/andreasjordan/demos/blob/master/AutomatedLab/CustomScripts/SQLServer_AlwaysOn.ps1" 30 | } 31 | 32 | Write-PSFMessage -Level Host -Message 'Change powerplan of cluster nodes to high performance' 33 | Set-DbaPowerPlan -ComputerName $ClusterNodes | Format-Table 34 | 35 | Write-PSFMessage -Level Host -Message 'Install SQL Server instances on cluster nodes' 36 | $installParams = @{ 37 | SqlInstance = $SqlInstances 38 | Version = 2017 39 | Feature = 'Engine' 40 | Path = $SQLServerSourcesPath 41 | UpdateSourcePath = $SQLServerPatchesPath 42 | EngineCredential = $sqlServerCredential 43 | AgentCredential = $sqlServerCredential 44 | AuthenticationMode = 'Mixed' 45 | Credential = $administratorCredential 46 | Restart = $true 47 | EnableException = $false 48 | } 49 | $installResult = Install-DbaInstance @installParams 50 | $installResult | Format-Table 51 | if ($false -in $installResult.Successful) { 52 | throw "Install-DbaInstance not successful" 53 | } 54 | 55 | Write-PSFMessage -Level Host -Message 'Grant instant file initialization rights to SQL Server service account on cluster nodes' 56 | Set-DbaPrivilege -ComputerName $ClusterNodes -Type IFI 57 | 58 | Write-PSFMessage -Level Host -Message 'Configure SQL Server instances: MaxMemory / MaxDop / CostThresholdForParallelism' 59 | Set-DbaMaxMemory -SqlInstance $SqlInstances -Max 2048 | Format-Table 60 | Set-DbaMaxDop -SqlInstance $SqlInstances | Format-Table 61 | Set-DbaSpConfigure -SqlInstance $SqlInstances -Name CostThresholdForParallelism -Value 50 | Format-Table 62 | 63 | Write-PSFMessage -Level Host -Message 'Restore and configure demo database' 64 | $null = Restore-DbaDatabase -SqlInstance $SqlInstances[0] -Path "$SampleDatabasesPath\AdventureWorks2017.bak" -DatabaseName $DatabaseName 65 | $null = Set-DbaDbOwner -SqlInstance $SqlInstances[0] -Database $DatabaseName -TargetLogin sa 66 | $null = Set-DbaDbRecoveryModel -SqlInstance $SqlInstances[0] -Database $DatabaseName -RecoveryModel Full 67 | $null = Backup-DbaDatabase -SqlInstance $SqlInstances[0] -Database $DatabaseName 68 | 69 | Write-PSFMessage -Level Host -Message 'finished' 70 | 71 | } catch { Write-PSFMessage -Level Warning -Message 'failed' -ErrorRecord $_ } 72 | -------------------------------------------------------------------------------- /AlwaysOn/02_setup_availability_group.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [string[]]$ClusterNodes = @('SQL01', 'SQL02'), 4 | [string[]]$SqlInstances = @('SQL01', 'SQL02'), 5 | [string]$BackupPath = '\\fs\Backup', 6 | [string]$DatabaseName = 'AdventureWorks', 7 | [string]$AvailabilityGroupName = 'AdventureSQL', 8 | [System.Net.IPAddress]$AvailabilityGroupIP = '192.168.3.71', 9 | [string[]]$EndpointUrls = @('TCP://192.168.3.31:5023', 'TCP://192.168.3.32:5023') 10 | ) 11 | 12 | $ErrorActionPreference = 'Stop' 13 | 14 | Import-Module -Name PSFramework 15 | Import-Module -Name dbatools 16 | 17 | $PSDefaultParameterValues['*-Dba*:EnableException'] = $true 18 | $PSDefaultParameterValues['*-Dba*:Confirm'] = $false 19 | 20 | try { 21 | 22 | Write-PSFMessage -Level Host -Message 'Configure SQL Server instance service to enable Always On' 23 | Enable-DbaAgHadr -SqlInstance $SqlInstances -Force | Format-Table 24 | 25 | $availabilityGroupParameters = @{ 26 | Primary = $SqlInstances[0] 27 | Secondary = $SqlInstances[1] 28 | Name = $AvailabilityGroupName 29 | IPAddress = $AvailabilityGroupIP 30 | Database = $DatabaseName 31 | ClusterType = 'Wsfc' 32 | ConfigureXESession = $true 33 | } 34 | 35 | # Special configuration for one of our clients: 36 | $availabilityGroupParameters['DtcSupport'] = $true 37 | $availabilityGroupParameters['ConnectionModeInSecondaryRole'] = 'AllowAllConnections' 38 | if ($EndpointUrls.Count -gt 0) { 39 | $availabilityGroupParameters['EndpointUrl'] = $EndpointUrls 40 | } 41 | 42 | Write-PSFMessage -Level Host -Message 'Add firewall rule for endpoint' 43 | $firewallConfig = @{ 44 | DisplayName = 'SQL Server HADR endpoint' 45 | Name = 'SQL Server HADR endpoint' 46 | Group = 'SQL Server' 47 | Enabled = 'True' 48 | Direction = 'Inbound' 49 | Protocol = 'TCP' 50 | LocalPort = '5022' 51 | } 52 | if ($EndpointUrls.Count -gt 0) { 53 | # All ports should be the same, so we only take the frist one. 54 | $firewallConfig.LocalPort = $EndpointUrls[0] -replace '^.*:(\d+)$', '$1' 55 | } 56 | Invoke-Command -ComputerName $ClusterNodes -ArgumentList $firewallConfig -ScriptBlock { 57 | Param ($FirewallRuleParameters) 58 | $null = New-NetFirewallRule @FirewallRuleParameters 59 | } 60 | 61 | Write-PSFMessage -Level Host -Message 'Create Always On Availability Group with automatic seeding' 62 | $availabilityGroupParameters['SeedingMode'] = 'Automatic' 63 | New-DbaAvailabilityGroup @availabilityGroupParameters | Format-Table 64 | Get-DbaAgReplica -SqlInstance $SqlInstances[0] -AvailabilityGroup $AvailabilityGroupName | Format-Table 65 | Get-DbaAgDatabase -SqlInstance $SqlInstances -AvailabilityGroup $AvailabilityGroupName -Database $DatabaseName | Format-Table 66 | 67 | 68 | # This is optional just to show some more features: 69 | 70 | Write-PSFMessage -Level Host -Message 'waiting...' 71 | Start-Sleep -Seconds 30 72 | 73 | Write-PSFMessage -Level Host -Message 'Drop Always On Availability Group' 74 | $null = Remove-DbaAvailabilityGroup -SqlInstance $SqlInstances[0] -AvailabilityGroup $AvailabilityGroupName 75 | $null = Remove-DbaDatabase -SqlInstance $SqlInstances[1] -Database $DatabaseName 76 | $null = Get-DbaEndpoint -SqlInstance $SqlInstances -Type DatabaseMirroring | Remove-DbaEndpoint -Confirm:$false 77 | 78 | Write-PSFMessage -Level Host -Message 'waiting...' 79 | Start-Sleep -Seconds 30 80 | 81 | Write-PSFMessage -Level Host -Message 'Create Always On Availability Group with manual seeding' 82 | $availabilityGroupParameters['SeedingMode'] = 'Manual' 83 | $availabilityGroupParameters['SharedPath'] = $BackupPath 84 | New-DbaAvailabilityGroup @availabilityGroupParameters | Format-Table 85 | Get-DbaAgReplica -SqlInstance $SqlInstances[0] -AvailabilityGroup $AvailabilityGroupName | Format-Table 86 | Get-DbaAgDatabase -SqlInstance $SqlInstances -AvailabilityGroup $AvailabilityGroupName -Database $DatabaseName | Format-Table 87 | 88 | 89 | Write-PSFMessage -Level Host -Message 'Add database to Always On Availability Group with backup-restore and automatic seeding' 90 | $null = New-DbaDatabase -SqlInstance $SqlInstances[0] -Name TestDB 91 | $null = Backup-DbaDatabase -SqlInstance $SqlInstances[0] -Database TestDB -Path $BackupPath -Type Full | Restore-DbaDatabase -SqlInstance $SqlInstances[1] -NoRecovery 92 | $null = Backup-DbaDatabase -SqlInstance $SqlInstances[0] -Database TestDB -Path $BackupPath -Type Log | Restore-DbaDatabase -SqlInstance $SqlInstances[1] -Continue -NoRecovery 93 | Add-DbaAgDatabase -SqlInstance $SqlInstances[0] -AvailabilityGroup $AvailabilityGroupName -Database TestDB -Secondary $SqlInstances[1] -SeedingMode Automatic | Format-Table 94 | Get-DbaAgDatabase -SqlInstance $SqlInstances -AvailabilityGroup $AvailabilityGroupName -Database TestDB | Format-Table 95 | 96 | 97 | Write-PSFMessage -Level Host -Message 'Configuring read only routing to use secondary for read only connections' 98 | $null = Set-DbaAgReplica -SqlInstance $SqlInstances[0] -AvailabilityGroup $AvailabilityGroupName -Replica $SqlInstances[0] -ReadonlyRoutingConnectionUrl "TCP://$($SqlInstances[0]):1433" 99 | $null = Set-DbaAgReplica -SqlInstance $SqlInstances[0] -AvailabilityGroup $AvailabilityGroupName -Replica $SqlInstances[1] -ReadonlyRoutingConnectionUrl "TCP://$($SqlInstances[1]):1433" 100 | $null = Set-DbaAgReplica -SqlInstance $SqlInstances[0] -AvailabilityGroup $AvailabilityGroupName -Replica $SqlInstances[0] -ReadOnlyRoutingList $SqlInstances[1], $SqlInstances[0] 101 | $null = Set-DbaAgReplica -SqlInstance $SqlInstances[0] -AvailabilityGroup $AvailabilityGroupName -Replica $SqlInstances[1] -ReadOnlyRoutingList $SqlInstances[0], $SqlInstances[1] 102 | 103 | Write-PSFMessage -Level Host -Message 'Testing read only routing to use secondary for read only connections' 104 | Connect-DbaInstance -SqlInstance $AvailabilityGroupName -Database $DatabaseName -ApplicationIntent ReadWrite | Format-Table 105 | Connect-DbaInstance -SqlInstance $AvailabilityGroupName -Database $DatabaseName -ApplicationIntent ReadOnly | Format-Table 106 | 107 | 108 | Write-PSFMessage -Level Host -Message 'finished' 109 | 110 | } catch { Write-PSFMessage -Level Warning -Message 'failed' -ErrorRecord $_ } 111 | 112 | -------------------------------------------------------------------------------- /AlwaysOn/02_setup_availability_group_SQL2016.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [string[]]$SqlInstances = @('SQL01\SQL2016', 'SQL02\SQL2016'), 4 | [string]$BackupPath = '\\fs\Backup', 5 | [string]$DatabaseName = 'AdventureWorks', 6 | [string]$AvailabilityGroupName = 'AdventureSQL2016', 7 | [System.Net.IPAddress]$AvailabilityGroupIP = '192.168.3.73', 8 | [string[]]$EndpointUrls = @('TCP://192.168.3.31:5025', 'TCP://192.168.3.32:5025') 9 | ) 10 | 11 | $ErrorActionPreference = 'Stop' 12 | 13 | Import-Module -Name PSFramework 14 | Import-Module -Name dbatools 15 | 16 | $PSDefaultParameterValues['*-Dba*:EnableException'] = $true 17 | $PSDefaultParameterValues['*-Dba*:Confirm'] = $false 18 | 19 | try { 20 | 21 | Write-PSFMessage -Level Host -Message 'Configure SQL Server instance service to enable Always On' 22 | Enable-DbaAgHadr -SqlInstance $SqlInstances -Force | Format-Table 23 | 24 | $availabilityGroupParameters = @{ 25 | Primary = $SqlInstances[0] 26 | Secondary = $SqlInstances[1] 27 | Name = $AvailabilityGroupName 28 | IPAddress = $AvailabilityGroupIP 29 | Port = 24330 # This is only needed if you have also run the 02_setup_availability_group.ps1 - as the other availability group already uses port 1433 30 | Database = $DatabaseName 31 | ClusterType = 'Wsfc' 32 | ConfigureXESession = $true 33 | } 34 | 35 | # Special configuration for one of our clients: 36 | $availabilityGroupParameters['DtcSupport'] = $true 37 | $availabilityGroupParameters['ConnectionModeInSecondaryRole'] = 'AllowAllConnections' 38 | if ($EndpointUrls.Count -gt 0) { 39 | $availabilityGroupParameters['EndpointUrl'] = $EndpointUrls 40 | } 41 | 42 | Write-PSFMessage -Level Host -Message 'Create Always On Availability Group with automatic seeding' 43 | $availabilityGroupParameters['SeedingMode'] = 'Automatic' 44 | New-DbaAvailabilityGroup @availabilityGroupParameters | Format-Table 45 | Get-DbaAgReplica -SqlInstance $SqlInstances[0] -AvailabilityGroup $AvailabilityGroupName | Format-Table 46 | Get-DbaAgDatabase -SqlInstance $SqlInstances -AvailabilityGroup $AvailabilityGroupName -Database $DatabaseName | Format-Table 47 | 48 | 49 | # This is optional just to show some more features: 50 | 51 | Write-PSFMessage -Level Host -Message 'waiting...' 52 | Start-Sleep -Seconds 30 53 | 54 | Write-PSFMessage -Level Host -Message 'Drop Always On Availability Group' 55 | $null = Remove-DbaAvailabilityGroup -SqlInstance $SqlInstances[0] -AvailabilityGroup $AvailabilityGroupName 56 | $null = Remove-DbaDatabase -SqlInstance $SqlInstances[1] -Database $DatabaseName 57 | $null = Get-DbaEndpoint -SqlInstance $SqlInstances -Type DatabaseMirroring | Remove-DbaEndpoint -Confirm:$false 58 | 59 | Write-PSFMessage -Level Host -Message 'waiting...' 60 | Start-Sleep -Seconds 30 61 | 62 | Write-PSFMessage -Level Host -Message 'Create Always On Availability Group with manual seeding' 63 | $availabilityGroupParameters['SeedingMode'] = 'Manual' 64 | $availabilityGroupParameters['SharedPath'] = $BackupPath 65 | New-DbaAvailabilityGroup @availabilityGroupParameters | Format-Table 66 | Get-DbaAgReplica -SqlInstance $SqlInstances[0] -AvailabilityGroup $AvailabilityGroupName | Format-Table 67 | Get-DbaAgDatabase -SqlInstance $SqlInstances -AvailabilityGroup $AvailabilityGroupName -Database $DatabaseName | Format-Table 68 | 69 | 70 | Write-PSFMessage -Level Host -Message 'Add database to Always On Availability Group with backup-restore and automatic seeding' 71 | $null = New-DbaDatabase -SqlInstance $SqlInstances[0] -Name TestDB 72 | $null = Backup-DbaDatabase -SqlInstance $SqlInstances[0] -Database TestDB -Path $BackupPath -Type Full | Restore-DbaDatabase -SqlInstance $SqlInstances[1] -NoRecovery 73 | $null = Backup-DbaDatabase -SqlInstance $SqlInstances[0] -Database TestDB -Path $BackupPath -Type Log | Restore-DbaDatabase -SqlInstance $SqlInstances[1] -Continue -NoRecovery 74 | Add-DbaAgDatabase -SqlInstance $SqlInstances[0] -AvailabilityGroup $AvailabilityGroupName -Database TestDB -Secondary $SqlInstances[1] -SeedingMode Automatic | Format-Table 75 | Get-DbaAgDatabase -SqlInstance $SqlInstances -AvailabilityGroup $AvailabilityGroupName -Database TestDB | Format-Table 76 | 77 | 78 | Write-PSFMessage -Level Host -Message 'finished' 79 | 80 | } catch { Write-PSFMessage -Level Warning -Message 'failed' -ErrorRecord $_ } 81 | -------------------------------------------------------------------------------- /AlwaysOn/02_setup_availability_group_SQL2017.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [string[]]$SqlInstances = @('SQL01\SQL2017', 'SQL02\SQL2017'), 4 | [string]$BackupPath = '\\fs\Backup', 5 | [string]$DatabaseName = 'AdventureWorks', 6 | [string]$AvailabilityGroupName = 'AdventureSQL2017', 7 | [System.Net.IPAddress]$AvailabilityGroupIP = '192.168.3.72', 8 | [string[]]$EndpointUrls = @('TCP://192.168.3.31:5024', 'TCP://192.168.3.32:5024') 9 | ) 10 | 11 | $ErrorActionPreference = 'Stop' 12 | 13 | Import-Module -Name PSFramework 14 | Import-Module -Name dbatools 15 | 16 | $PSDefaultParameterValues['*-Dba*:EnableException'] = $true 17 | $PSDefaultParameterValues['*-Dba*:Confirm'] = $false 18 | 19 | try { 20 | 21 | Write-PSFMessage -Level Host -Message 'Configure SQL Server instance service to enable Always On' 22 | Enable-DbaAgHadr -SqlInstance $SqlInstances -Force | Format-Table 23 | 24 | $availabilityGroupParameters = @{ 25 | Primary = $SqlInstances[0] 26 | Secondary = $SqlInstances[1] 27 | Name = $AvailabilityGroupName 28 | IPAddress = $AvailabilityGroupIP 29 | Port = 14330 # This is only needed if you have also run the 02_setup_availability_group.ps1 - as the other availability group already uses port 1433 30 | Database = $DatabaseName 31 | ClusterType = 'Wsfc' 32 | ConfigureXESession = $true 33 | } 34 | 35 | # Special configuration for one of our clients: 36 | $availabilityGroupParameters['DtcSupport'] = $true 37 | $availabilityGroupParameters['ConnectionModeInSecondaryRole'] = 'AllowAllConnections' 38 | if ($EndpointUrls.Count -gt 0) { 39 | $availabilityGroupParameters['EndpointUrl'] = $EndpointUrls 40 | } 41 | 42 | Write-PSFMessage -Level Host -Message 'Create Always On Availability Group with automatic seeding' 43 | $availabilityGroupParameters['SeedingMode'] = 'Automatic' 44 | New-DbaAvailabilityGroup @availabilityGroupParameters | Format-Table 45 | Get-DbaAgReplica -SqlInstance $SqlInstances[0] -AvailabilityGroup $AvailabilityGroupName | Format-Table 46 | Get-DbaAgDatabase -SqlInstance $SqlInstances -AvailabilityGroup $AvailabilityGroupName -Database $DatabaseName | Format-Table 47 | 48 | 49 | # This is optional just to show some more features: 50 | 51 | Write-PSFMessage -Level Host -Message 'waiting...' 52 | Start-Sleep -Seconds 30 53 | 54 | Write-PSFMessage -Level Host -Message 'Drop Always On Availability Group' 55 | $null = Remove-DbaAvailabilityGroup -SqlInstance $SqlInstances[0] -AvailabilityGroup $AvailabilityGroupName 56 | $null = Remove-DbaDatabase -SqlInstance $SqlInstances[1] -Database $DatabaseName 57 | $null = Get-DbaEndpoint -SqlInstance $SqlInstances -Type DatabaseMirroring | Remove-DbaEndpoint -Confirm:$false 58 | 59 | Write-PSFMessage -Level Host -Message 'waiting...' 60 | Start-Sleep -Seconds 30 61 | 62 | Write-PSFMessage -Level Host -Message 'Create Always On Availability Group with manual seeding' 63 | $availabilityGroupParameters['SeedingMode'] = 'Manual' 64 | $availabilityGroupParameters['SharedPath'] = $BackupPath 65 | New-DbaAvailabilityGroup @availabilityGroupParameters | Format-Table 66 | Get-DbaAgReplica -SqlInstance $SqlInstances[0] -AvailabilityGroup $AvailabilityGroupName | Format-Table 67 | Get-DbaAgDatabase -SqlInstance $SqlInstances -AvailabilityGroup $AvailabilityGroupName -Database $DatabaseName | Format-Table 68 | 69 | 70 | Write-PSFMessage -Level Host -Message 'Add database to Always On Availability Group with backup-restore and automatic seeding' 71 | $null = New-DbaDatabase -SqlInstance $SqlInstances[0] -Name TestDB 72 | $null = Backup-DbaDatabase -SqlInstance $SqlInstances[0] -Database TestDB -Path $BackupPath -Type Full | Restore-DbaDatabase -SqlInstance $SqlInstances[1] -NoRecovery 73 | $null = Backup-DbaDatabase -SqlInstance $SqlInstances[0] -Database TestDB -Path $BackupPath -Type Log | Restore-DbaDatabase -SqlInstance $SqlInstances[1] -Continue -NoRecovery 74 | Add-DbaAgDatabase -SqlInstance $SqlInstances[0] -AvailabilityGroup $AvailabilityGroupName -Database TestDB -Secondary $SqlInstances[1] -SeedingMode Automatic | Format-Table 75 | Get-DbaAgDatabase -SqlInstance $SqlInstances -AvailabilityGroup $AvailabilityGroupName -Database TestDB | Format-Table 76 | 77 | 78 | Write-PSFMessage -Level Host -Message 'finished' 79 | 80 | } catch { Write-PSFMessage -Level Warning -Message 'failed' -ErrorRecord $_ } 81 | 82 | -------------------------------------------------------------------------------- /AlwaysOn/03_add_SQL03_to_Cluster_and_AvailabilityGroup.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [string]$DomainName = 'ORDIX', 4 | [string]$DomainController = 'DC', 5 | [string[]]$ClusterNodes = @('SQL01', 'SQL02'), 6 | [string[]]$SqlInstances = @('SQL01', 'SQL02'), 7 | [string]$NewClusterNode = 'SQL03', 8 | [string]$NewSqlInstance = 'SQL03', 9 | [string]$ClusterName = 'CLUSTER1', 10 | [string]$SQLServerServiceAccount = 'gMSA-SQLServer', 11 | [SecureString]$AdminPassword = (ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force), 12 | [string]$SQLServerSourcesPath = '\\fs\Software\SQLServer\ISO', 13 | [string]$SQLServerPatchesPath = '\\fs\Software\SQLServer\CU', 14 | [string]$AvailabilityGroupName = 'AdventureSQL', 15 | [string]$BackupPath = '\\fs\Backup' 16 | ) 17 | 18 | $ErrorActionPreference = 'Stop' 19 | 20 | Import-Module -Name PSFramework 21 | Import-Module -Name dbatools 22 | 23 | $PSDefaultParameterValues['*-Dba*:EnableException'] = $true 24 | $PSDefaultParameterValues['*-Dba*:Confirm'] = $false 25 | 26 | try { 27 | 28 | Write-PSFMessage -Level Host -Message 'Install cluster feature on new node' 29 | $installResult = Invoke-Command -ComputerName $NewClusterNode -ScriptBlock { Install-WindowsFeature -Name Failover-Clustering -IncludeManagementTools } 30 | $installResult | Format-Table 31 | if ( $installResult.RestartNeeded -eq 'Yes' ) { 32 | # Restart is needed on Windows Server 2019 33 | Restart-Computer -ComputerName $NewClusterNode 34 | Start-Sleep -Seconds 60 35 | } 36 | 37 | Write-PSFMessage -Level Host -Message 'Add new node to cluster' 38 | Add-ClusterNode -Cluster $ClusterName -Name $NewClusterNode 39 | # This should output something, but it does not 40 | 41 | Write-PSFMessage -Level Host -Message 'Lets see if we have a new node' 42 | Get-ClusterNode -Cluster $ClusterName | Format-Table 43 | # Yes, we have 44 | 45 | 46 | # Begin of code from 01_setup_instances.ps1 47 | 48 | $administratorCredential = [PSCredential]::new("$DomainName\Admin", $AdminPassword) 49 | $sqlServerCredential = [PSCredential]::new("$DomainName\$SQLServerServiceAccount$", [SecureString]::new()) 50 | 51 | Write-PSFMessage -Level Host -Message 'Change powerplan of new cluster node to high performance' 52 | Set-DbaPowerPlan -ComputerName $NewClusterNode | Format-Table 53 | 54 | Write-PSFMessage -Level Host -Message 'Install SQL Server instances on new cluster node' 55 | $installParams = @{ 56 | SqlInstance = $NewSqlInstance 57 | Version = 2022 58 | Feature = 'Engine' 59 | Path = $SQLServerSourcesPath 60 | UpdateSourcePath = $SQLServerPatchesPath 61 | EngineCredential = $sqlServerCredential 62 | AgentCredential = $sqlServerCredential 63 | AuthenticationMode = 'Mixed' 64 | Credential = $administratorCredential 65 | Restart = $true 66 | EnableException = $false 67 | } 68 | $installResult = Install-DbaInstance @installParams 69 | $installResult | Format-Table 70 | if ($false -in $installResult.Successful) { 71 | throw "Install-DbaInstance not successful" 72 | } 73 | 74 | Write-PSFMessage -Level Host -Message 'Grant instant file initialization rights to SQL Server service account on new cluster node' 75 | Set-DbaPrivilege -ComputerName $NewClusterNode -Type IFI 76 | 77 | Write-PSFMessage -Level Host -Message 'Configure SQL Server instances: MaxMemory / MaxDop / CostThresholdForParallelism' 78 | Set-DbaMaxMemory -SqlInstance $NewSqlInstance -Max 2048 | Format-Table 79 | Set-DbaMaxDop -SqlInstance $NewSqlInstance | Format-Table 80 | Set-DbaSpConfigure -SqlInstance $NewSqlInstance -Name CostThresholdForParallelism -Value 50 | Format-Table 81 | 82 | # End of code from 01_setup_instances.ps1 83 | 84 | 85 | # Begin of code from 02_setup_availability_group.ps1 86 | 87 | Write-PSFMessage -Level Host -Message 'Configure SQL Server instance service to enable Always On' 88 | Enable-DbaAgHadr -SqlInstance $NewSqlInstance -Force | Format-Table 89 | 90 | # End of code from 02_setup_availability_group.ps1 91 | 92 | 93 | $ag = Get-DbaAvailabilityGroup -SqlInstance $SqlInstances -AvailabilityGroup $AvailabilityGroupName | Where-Object LocalReplicaRole -eq Primary 94 | $primaryReplica = Get-DbaAgReplica -SqlInstance $ag.Parent -AvailabilityGroup $AvailabilityGroupName | Where-Object Role -eq Primary 95 | 96 | $replicaParameters = @{ 97 | SqlInstance = $NewSqlInstance 98 | AvailabilityMode = $primaryReplica.AvailabilityMode 99 | FailoverMode = $primaryReplica.FailoverMode 100 | BackupPriority = $primaryReplica.BackupPriority 101 | ConnectionModeInPrimaryRole = $primaryReplica.ConnectionModeInPrimaryRole 102 | ConnectionModeInSecondaryRole = $primaryReplica.ConnectionModeInSecondaryRole 103 | SeedingMode = 'Manual' 104 | ConfigureXESession = $true 105 | } 106 | 107 | # Special configuration for one of our clients: 108 | $replicaParameters['EndpointUrl'] = 'TCP://192.168.3.33:5023' 109 | 110 | Write-PSFMessage -Level Host -Message 'Adding replica to Always On Availability Group with manual seeding' 111 | $ag | Add-DbaAgReplica @replicaParameters | Format-Table 112 | 113 | # Wait for new replica to connect to Availability Group 114 | Write-PSFMessage -Level Host -Message 'waiting...' 115 | Start-Sleep -Seconds 30 116 | 117 | Write-PSFMessage -Level Host -Message 'Adding databases to Always On Availability Group with manual seeding' 118 | Add-DbaAgDatabase -SqlInstance $primaryReplica.SqlInstance -AvailabilityGroup $AvailabilityGroupName -Database $ag.AvailabilityDatabases.Name -SharedPath $BackupPath | Format-Table 119 | Get-DbaAgReplica -SqlInstance $primaryReplica.SqlInstance -AvailabilityGroup $AvailabilityGroupName | Format-Table 120 | Get-DbaAgDatabase -SqlInstance $NewSqlInstance -AvailabilityGroup $AvailabilityGroupName | Format-Table 121 | 122 | 123 | # This is optional just to show some more features: 124 | 125 | Write-PSFMessage -Level Host -Message 'waiting...' 126 | Start-Sleep -Seconds 30 127 | 128 | $null = Remove-DbaAgReplica -SqlInstance $ag.Parent -AvailabilityGroup $AvailabilityGroupName -Replica $NewSqlInstance 129 | $null = Get-DbaDatabase -SqlInstance $NewSqlInstance -Database $ag.AvailabilityDatabases.Name | Remove-DbaDatabase 130 | $null = Get-DbaEndpoint -SqlInstance $NewSqlInstance -Type DatabaseMirroring | Remove-DbaEndpoint -Confirm:$false 131 | 132 | Write-PSFMessage -Level Host -Message 'Adding replica to Always On Availability Group with automatic seeding' 133 | $replicaParameters['SeedingMode'] = 'Automatic' 134 | $ag | Add-DbaAgReplica @replicaParameters | Format-Table 135 | 136 | # Wait for new replica to connect to Availability Group and for automatic seeding to move databases to new replica 137 | Write-PSFMessage -Level Host -Message 'waiting...' 138 | Start-Sleep -Seconds 30 139 | 140 | Get-DbaAgReplica -SqlInstance $primaryReplica.SqlInstance -AvailabilityGroup $AvailabilityGroupName | Format-Table 141 | Get-DbaAgDatabase -SqlInstance $NewSqlInstance -AvailabilityGroup $AvailabilityGroupName | Format-Table 142 | 143 | 144 | Write-PSFMessage -Level Host -Message 'finished' 145 | 146 | } catch { Write-PSFMessage -Level Warning -Message 'failed' -ErrorRecord $_ } 147 | -------------------------------------------------------------------------------- /AlwaysOn/99_remove.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [string]$DomainName = 'ORDIX', 4 | [string]$DomainController = 'DC', 5 | [string]$ClusterName = 'CLUSTER1', 6 | [SecureString]$AdminPassword = (ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force), 7 | [string]$SQLServerSourcesPath = '\\fs\Software\SQLServer\ISO', 8 | [string]$BackupPath = '\\fs\Backup' 9 | ) 10 | 11 | $ErrorActionPreference = 'Stop' 12 | 13 | Import-Module -Name PSFramework 14 | Import-Module -Name dbatools 15 | 16 | $PSDefaultParameterValues['*-Dba*:Confirm'] = $false 17 | 18 | $administratorCredential = New-Object -TypeName PSCredential -ArgumentList "$DomainName\Admin", $AdminPassword 19 | 20 | Write-PSFMessage -Level Host -Message 'Get Cluster Nodes' 21 | if ($PSVersionTable.PSVersion.Major -eq 5) { 22 | $clusterNodes = Get-Cluster -Name $ClusterName | Get-ClusterNode 23 | } else { 24 | $clusterNodes = Get-ClusterNode -Cluster $ClusterName 25 | } 26 | 27 | Write-PSFMessage -Level Host -Message 'Get SQL Server instances on cluster nodes' 28 | $sqlInstances = Find-DbaInstance -ComputerName $clusterNodes.Name 29 | 30 | Write-PSFMessage -Level Host -Message 'Get Availability Groups' 31 | $ags = Get-DbaAvailabilityGroup -SqlInstance $sqlInstances.SqlInstance | Where-Object LocalReplicaRole -eq 'Primary' 32 | 33 | foreach ($ag in $ags) { 34 | Write-PSFMessage -Level Host -Message "Remove Availability Group $($ag.AvailabilityGroup) to remove cluster resource" 35 | $ag | Remove-DbaAvailabilityGroup -Confirm:$false 36 | Get-ADComputer -Filter "Name -eq '$($ag.AvailabilityGroup)'" | Remove-ADComputer -Confirm:$false 37 | } 38 | 39 | foreach ($inst in $sqlInstances) { 40 | Write-PSFMessage -Level Host -Message "Remove SQL Server instance $($inst.SqlInstance)" 41 | 42 | # What information do we need? 43 | # * Version for Install-DbaInstance to select the correct setup.exe 44 | # * InstancePath for Remove-Item to remove all left over files 45 | try { 46 | # Try to get information from running instance 47 | $server = Connect-DbaInstance -SqlInstance $inst.SqlInstance 48 | $instanceVersion = ($server.GetSqlServerVersionName() -split ' ')[-1] 49 | if (-not $instanceVersion) { $instanceVersion = 2022 } 50 | $instancePath = $server.RootDirectory -replace 'MSSQL$', '' 51 | } catch { 52 | # Fallback to information about the service 53 | $service = Get-DbaService -ComputerName $inst.ComputerName -InstanceName $inst.InstanceName -Type Engine -EnableException 54 | $instanceVersion = switch ($service.BinaryPath -replace '^.*MSSQL(\d\d).*$', '$1') { 16 { 2022 } 15 { 2019 } 14 { 2017 } 13 { 2016 } 12 { 2014 } 11 { 2012 } } 55 | $instancePath = $service.BinaryPath -replace '^"?(.*)MSSQL\\Binn\\sqlservr\.exe.*$', '$1' 56 | } 57 | $params = @{ 58 | SqlInstance = $inst.SqlInstance 59 | Version = $instanceVersion 60 | Feature = 'Engine' 61 | Configuration = @{ ACTION = 'Uninstall' } 62 | Path = $SQLServerSourcesPath 63 | Restart = $true 64 | Credential = $administratorCredential 65 | Confirm = $false 66 | } 67 | $result = Install-DbaInstance @params 68 | if (-not $result.Successful) { 69 | $result.Log | Set-Clipboard 70 | throw "Uninstall failed, see clipboard for details" 71 | } 72 | 73 | # Remove firewall rule 74 | Get-DbaFirewallRule -SqlInstance $inst.SqlInstance | Remove-DbaFirewallRule 75 | 76 | # Remove directory 77 | Invoke-Command -ComputerName $inst.ComputerName -ScriptBlock { 78 | param($path) 79 | Remove-Item -Path $path -Recurse -Force 80 | } -ArgumentList $instancePath 81 | } 82 | 83 | foreach ($node in $clusterNodes) { 84 | Write-PSFMessage -Level Host -Message "Remove SQL Server base directory on $($node.Name)" 85 | Invoke-Command -ComputerName $node.Name -ScriptBlock { Remove-Item -Path 'C:\Program Files\Microsoft SQL Server' -Recurse -Force } 86 | } 87 | 88 | Write-PSFMessage -Level Host -Message "Remove backups from backup directory" 89 | Get-ChildItem -Path $BackupPath | Where-Object -Property Name -Match -Value '_\d{12}.(bak|trn)$' | Remove-Item 90 | 91 | Write-PSFMessage -Level Host -Message "Remove cluster" 92 | if ($PSVersionTable.PSVersion.Major -eq 5) { 93 | Get-Cluster -Name $ClusterName | Remove-Cluster -Force 94 | } else { 95 | Remove-Cluster -Cluster $ClusterName -Force 96 | } 97 | Get-ADComputer -Filter "Name -eq '$ClusterName'" | Remove-ADComputer -Confirm:$false 98 | Invoke-Command -ComputerName $DomainController -ScriptBlock { Remove-SmbShare -Name "WindowsClusterQuorum_$using:ClusterName" -Force } 99 | Invoke-Command -ComputerName $DomainController -ScriptBlock { Remove-Item -Path "C:\WindowsClusterQuorum_$using:ClusterName" -Recurse } 100 | Invoke-Command -ComputerName $clusterNodes.Name -ScriptBlock { Remove-WindowsFeature -Name Failover-Clustering -IncludeManagementTools } | Format-Table 101 | 102 | Write-PSFMessage -Level Host -Message 'Restart server' 103 | Restart-Computer -ComputerName $clusterNodes.Name -Force 104 | 105 | 106 | # Maybe needed: 107 | # Enable-WSManCredSSP -Role Client -DelegateComputer $ClusterNodes -Force | Out-Null 108 | # Invoke-Command -ComputerName $ClusterNodes -ScriptBlock { Enable-WSManCredSSP -Role Server -Force } | Out-Null 109 | 110 | # Should be reset as well: 111 | # Set-DbaPrivilege -ComputerName $ClusterNodes -Type IFI 112 | -------------------------------------------------------------------------------- /AlwaysOn/README.md: -------------------------------------------------------------------------------- 1 | # Automated test for Always On Availability Groups 2 | 3 | I use these scripts to have a repeatable test for the dbatools commands related to Always On Availability Groups. 4 | 5 | I use [this](../AutomatedLab/CustomScripts/SQLServer_AlwaysOn.ps1) lab to run the scripts. 6 | 7 | I have tested the scripts with the current 2.1.5 version of dbatools. 8 | -------------------------------------------------------------------------------- /AlwaysOn_DatabaseOwner/01_database_owner.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Stop' 2 | 3 | Import-Module -Name dbatools 4 | 5 | $SqlInstances = 'SRV1', 'SRV2' 6 | $BackupPath = '\\WIN10\SQLServerBackups' 7 | $DatabaseName = 'Test01' 8 | $AvailabilityGroupName = 'AdventureSQL' 9 | 10 | 11 | # We need three logins with sysadmin role, one for creating the database, one for restoring the database and one for as the new owner 12 | 13 | $credAdmin1 = Get-Credential -Message 'SQL Admin 1' -UserName admin1 14 | $credAdmin2 = Get-Credential -Message 'SQL Admin 2' -UserName admin2 15 | $credAdmin3 = Get-Credential -Message 'SQL Admin 2' -UserName admin3 16 | 17 | 18 | # Take care that we have mixed authentication mode 19 | 20 | $server = Connect-DbaInstance -SqlInstance $SqlInstances 21 | foreach ($srv in $server) { 22 | if ($srv.LoginMode -ne 'Mixed') { 23 | $srv.LoginMode = 'Mixed' 24 | $srv.Alter() 25 | $null = Restart-DbaService -ComputerName $srv.ComputerName -InstanceName $srv.DbaInstanceName -Type Engine -Force 26 | } 27 | } 28 | 29 | 30 | # Take care the the first instance is the primary of the availability group 31 | 32 | if ((Get-DbaAvailabilityGroup -SqlInstance $server[0] -AvailabilityGroup $AvailabilityGroupName).LocalReplicaRole -ne 'Primary') { 33 | $null = Invoke-DbaAgFailover -SqlInstance $server[0] -AvailabilityGroup $AvailabilityGroupName -Confirm:$false 34 | } 35 | 36 | 37 | # Create logins, add them to sysadmin and create a connection with that login 38 | 39 | $loginAdmin1 = New-DbaLogin -SqlInstance $server[0] -Login $credAdmin1.UserName -SecurePassword $credAdmin1.Password 40 | $null = New-DbaLogin -SqlInstance $server[1] -Login $credAdmin1.UserName -SecurePassword $credAdmin1.Password -Sid $loginAdmin1.Sid 41 | $null = Set-DbaLogin -SqlInstance $server -Login $credAdmin1.UserName -AddRole sysadmin 42 | $serverAdmin1 = Connect-DbaInstance -SqlInstance $SqlInstances -SqlCredential $credAdmin1 43 | 44 | $loginAdmin2 = New-DbaLogin -SqlInstance $server[0] -Login $credAdmin2.UserName -SecurePassword $credAdmin2.Password 45 | $null = New-DbaLogin -SqlInstance $server[1] -Login $credAdmin2.UserName -SecurePassword $credAdmin2.Password -Sid $loginAdmin2.Sid 46 | $null = Set-DbaLogin -SqlInstance $server -Login $credAdmin2.UserName -AddRole sysadmin 47 | $serverAdmin2 = Connect-DbaInstance -SqlInstance $SqlInstances -SqlCredential $credAdmin2 48 | 49 | $loginAdmin3 = New-DbaLogin -SqlInstance $server[0] -Login $credAdmin3.UserName -SecurePassword $credAdmin3.Password 50 | $null = New-DbaLogin -SqlInstance $server[1] -Login $credAdmin3.UserName -SecurePassword $credAdmin3.Password -Sid $loginAdmin3.Sid 51 | $null = Set-DbaLogin -SqlInstance $server -Login $credAdmin3.UserName -AddRole sysadmin 52 | $serverAdmin3 = Connect-DbaInstance -SqlInstance $SqlInstances -SqlCredential $credAdmin3 53 | 54 | 55 | # Create the database as admin1 and create the first backups 56 | 57 | $null = New-DbaDatabase -SqlInstance $serverAdmin1[0] -Name $DatabaseName 58 | $fullBackup = Backup-DbaDatabase -SqlInstance $serverAdmin1[0] -Database $DatabaseName -Path $BackupPath -Type Full 59 | $logBackup = Backup-DbaDatabase -SqlInstance $serverAdmin1[0] -Database $DatabaseName -Path $BackupPath -Type Log 60 | 61 | 62 | # Restore the database as admin2 and add the database to the availability group 63 | 64 | $null = $fullBackup | Restore-DbaDatabase -SqlInstance $serverAdmin2[1] -NoRecovery 65 | $null = $logBackup | Restore-DbaDatabase -SqlInstance $serverAdmin2[1] -NoRecovery -Continue 66 | $null = Add-DbaAgDatabase -SqlInstance $serverAdmin2[0] -AvailabilityGroup $AvailabilityGroupName -Database $DatabaseName -Secondary $serverAdmin2[1] -SeedingMode Automatic 67 | 68 | 69 | # Get information about the database 70 | 71 | Get-DbaAgDatabase -SqlInstance $server -AvailabilityGroup $AvailabilityGroupName -Database $DatabaseName | Format-Table -Property SqlInstance, Name, SynchronizationState 72 | Get-DbaDatabase -SqlInstance $server -Database $DatabaseName | Format-Table -Property SqlInstance, Name, Owner 73 | 74 | <# 75 | 76 | SqlInstance Name SynchronizationState 77 | ----------- ---- -------------------- 78 | SRV1 Test01 Synchronized 79 | SRV2 Test01 Synchronized 80 | 81 | 82 | SqlInstance Name Owner 83 | ----------- ---- ----- 84 | SRV1 Test01 admin1 85 | SRV2 Test01 admin2 86 | 87 | #> 88 | 89 | # Result: Owner is different 90 | 91 | 92 | # Change the owner of the database which is only possible at the primary instance 93 | 94 | $null = Set-DbaDbOwner -SqlInstance $server[0] -Database $DatabaseName -TargetLogin $credAdmin3.UserName 95 | Get-DbaDatabase -SqlInstance $server -Database $DatabaseName | Format-Table -Property SqlInstance, Name, Owner # Ups, not updated - but maybe its only the SMO... 96 | Get-DbaDatabase -SqlInstance $SqlInstances -Database $DatabaseName | Format-Table -Property SqlInstance, Name, Owner # Yes, that's a bug... 97 | # Let's refresh the SMO (like hitting F5 in SSMS): 98 | $server[0].Databases[$DatabaseName].Refresh() 99 | Get-DbaDatabase -SqlInstance $server -Database $DatabaseName | Format-Table -Property SqlInstance, Name, Owner # Ok, now the correct info is there 100 | 101 | <# 102 | 103 | SqlInstance Name Owner 104 | ----------- ---- ----- 105 | SRV1 Test01 admin3 106 | SRV2 Test01 admin2 107 | 108 | #> 109 | 110 | # Result: The owner is not changed at the secondary instance. So this info is not part of the database but part of the instance 111 | 112 | # Here is where the info is stored 113 | 114 | $server.Query("SELECT @@SERVERNAME AS server_name, db.name AS database_name, sp.name AS owner_name FROM sys.databases db JOIN sys.server_principals sp ON db.owner_sid = sp.sid WHERE db.name = '$DatabaseName'") 115 | 116 | <# 117 | 118 | server_name database_name owner_name 119 | ----------- ------------- ---------- 120 | SRV1 Test01 admin3 121 | SRV2 Test01 admin2 122 | 123 | #> 124 | 125 | 126 | # How to change the owner on the secondary? Fail over, change owner, fail back 127 | 128 | $null = Invoke-DbaAgFailover -SqlInstance $server[1] -AvailabilityGroup $AvailabilityGroupName -Confirm:$false 129 | $null = Set-DbaDbOwner -SqlInstance $server[1] -Database $DatabaseName -TargetLogin $credAdmin3.UserName # Ups, SMO is not up to date again... 130 | $null = Set-DbaDbOwner -SqlInstance $SqlInstances[1] -Database $DatabaseName -TargetLogin $credAdmin3.UserName # That works 131 | $null = Invoke-DbaAgFailover -SqlInstance $server[0] -AvailabilityGroup $AvailabilityGroupName -Confirm:$false 132 | 133 | 134 | # Let's see the result (and use new SMOs to get up to date data) 135 | 136 | Get-DbaAgReplica -SqlInstance $SqlInstances[0] -AvailabilityGroup $AvailabilityGroupName | Format-Table -Property Name, AvailabilityGroup, Role 137 | Get-DbaDatabase -SqlInstance $SqlInstances -Database $DatabaseName | Format-Table -Property SqlInstance, Name, Owner 138 | 139 | <# 140 | 141 | Name AvailabilityGroup Role 142 | ---- ----------------- ---- 143 | SRV1 AdventureSQL Primary 144 | SRV2 AdventureSQL Secondary 145 | 146 | 147 | SqlInstance Name Owner 148 | ----------- ---- ----- 149 | SRV1 Test01 admin3 150 | SRV2 Test01 admin3 151 | 152 | #> 153 | 154 | 155 | # Cleanup 156 | 157 | $null = Remove-DbaAgDatabase -SqlInstance $SqlInstances[0] -Database $DatabaseName -AvailabilityGroup $AvailabilityGroupName -Confirm:$false 158 | $null = Remove-DbaDatabase -SqlInstance $SqlInstances -Database $DatabaseName -Confirm:$false 159 | $null = Remove-DbaLogin -SqlInstance $SqlInstances -Login $credAdmin1.UserName, $credAdmin2.UserName, $credAdmin3.UserName -Force 160 | Remove-DbaDbBackupRestoreHistory -SqlInstance $SqlInstances -Database $DatabaseName -Confirm:$false 161 | Get-ChildItem -Path $BackupPath -Filter ($DatabaseName + '*') | Remove-Item 162 | -------------------------------------------------------------------------------- /AlwaysOn_MultiInstance/00_setup_cluster.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [string]$DomainName = 'COMPANY', 4 | [string]$DomainController = 'DOM1', 5 | [string[]]$ClusterNodes = @('SRV1', 'SRV2'), 6 | [string]$ClusterName = 'SQLCluster', 7 | [string]$ClusterIP = '192.168.3.70' 8 | ) 9 | 10 | function Write-LocalWarning { 11 | param ( 12 | [string]$Message 13 | ) 14 | Write-Warning -Message ('{0}: {1}' -f (Get-Date), $Message) 15 | } 16 | 17 | function Write-LocalHost { 18 | param ( 19 | [string]$Message, 20 | [string]$ForegroundColor = 'Yellow' 21 | ) 22 | Microsoft.PowerShell.Utility\Write-Host -Object ('{0}: {1}' -f (Get-Date), $Message) -ForegroundColor $ForegroundColor 23 | } 24 | 25 | function Write-LocalVerbose { 26 | param ( 27 | [string]$Message 28 | ) 29 | Write-Verbose -Message ('{0}: {1}' -f (Get-Date), $Message) 30 | } 31 | 32 | $ErrorActionPreference = 'Stop' 33 | 34 | Write-LocalHost -Message 'Install cluster freature on each node' 35 | Invoke-Command -ComputerName $ClusterNodes -ScriptBlock { Install-WindowsFeature -Name Failover-Clustering -IncludeManagementTools } | Format-Table 36 | 37 | Write-LocalHost -Message 'Run cluster test and display result in web browser' 38 | $clusterTest = Test-Cluster -Node $ClusterNodes 39 | &$clusterTest.FullName 40 | 41 | Write-LocalHost -Message 'Create the cluster' 42 | $cluster = New-Cluster -Name $ClusterName -Node $ClusterNodes -StaticAddress $ClusterIP 43 | 44 | Write-LocalHost -Message 'Create a share as cluster quorum and configure the cluster' 45 | Invoke-Command -ComputerName $DomainController -ScriptBlock { 46 | $null = New-Item -Path "C:\WindowsClusterQuorum_$using:ClusterName" -ItemType Directory 47 | $null = New-SmbShare -Path "C:\WindowsClusterQuorum_$using:ClusterName" -Name "WindowsClusterQuorum_$using:ClusterName" 48 | $null = Grant-SmbShareAccess -Name "WindowsClusterQuorum_$using:ClusterName" -AccountName "$using:DomainName\$using:ClusterName$" -AccessRight Full -Force 49 | } 50 | $cluster | Set-ClusterQuorum -NodeAndFileShareMajority "\\$DomainController\WindowsClusterQuorum_$ClusterName" | Format-List 51 | 52 | Write-LocalHost -Message 'finished' 53 | -------------------------------------------------------------------------------- /AlwaysOn_MultiInstance/01_setup_instances.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [string[]]$ClusterNodes = @('SRV1', 'SRV2'), 4 | [string[]]$InstanceNames = @('MSSQLSERVER', 'SQL2017', 'SQL2016', 'SQL2014'), 5 | [string[]]$SqlVersions = @('2019', '2017', '2016', '2014'), 6 | [PSCredential]$AdministratorCredential = (New-Object -TypeName PSCredential -ArgumentList "COMPANY\Administrator", (ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force)), 7 | [PSCredential]$SqlServerCredential = (New-Object -TypeName PSCredential -ArgumentList "COMPANY\SQLServer", (ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force)), 8 | [string]$SQLServerSourcesPath = '\\WIN10\SQLServerSources', 9 | [string]$SQLServerPatchesPath = '\\WIN10\SQLServerPatches', 10 | [string]$BackupPath = '\\WIN10\SQLServerBackups', 11 | [string]$DatabaseName = 'AdventureWorks' 12 | ) 13 | 14 | function Write-LocalWarning { 15 | param ( 16 | [string]$Message 17 | ) 18 | Write-Warning -Message ('{0}: {1}' -f (Get-Date), $Message) 19 | } 20 | 21 | function Write-LocalHost { 22 | param ( 23 | [string]$Message, 24 | [string]$ForegroundColor = 'Yellow' 25 | ) 26 | Microsoft.PowerShell.Utility\Write-Host -Object ('{0}: {1}' -f (Get-Date), $Message) -ForegroundColor $ForegroundColor 27 | } 28 | 29 | function Write-LocalVerbose { 30 | param ( 31 | [string]$Message 32 | ) 33 | Write-Verbose -Message ('{0}: {1}' -f (Get-Date), $Message) 34 | } 35 | 36 | $ErrorActionPreference = 'Stop' 37 | 38 | $instances = @() 39 | foreach ( $nr in 0 .. ($InstanceNames.Count - 1) ) { 40 | $instances += [PSCustomObject]@{ 41 | InstanceName = $InstanceNames[$nr] 42 | SqlVersion = $SqlVersions[$nr] 43 | } 44 | } 45 | 46 | $SQLServerServiceAccountName = ($SqlServerCredential.UserName -split '\\')[1] 47 | if ( $null -eq (Get-ADUser -Filter "Name -eq '$SQLServerServiceAccountName'") ) { 48 | Write-LocalHost -Message 'Creating user for SQL Server service account and grant access to backup share' 49 | New-ADUser -Name $SQLServerServiceAccountName -AccountPassword $sqlServerCredential.Password -PasswordNeverExpires:$true -Enabled:$true 50 | $null = Grant-SmbShareAccess -Name SQLServerBackups -AccountName $SqlServerCredential.UserName -AccessRight Full -Force 51 | } 52 | 53 | Write-LocalHost -Message 'Import module dbatools' 54 | Import-Module -Name dbatools -MinimumVersion 1.0.124 55 | 56 | Write-LocalHost -Message 'Change powerplan of cluster nodes to high performance' 57 | Set-DbaPowerPlan -ComputerName $ClusterNodes | Format-Table 58 | 59 | foreach ( $instance in $instances ) { 60 | $sqlInstances = @(); 61 | foreach ( $node in $ClusterNodes ) { 62 | $sqlInstances += "$node\$($instance.InstanceName)" 63 | } 64 | 65 | Write-LocalHost -Message "Install SQL Server $($instance.SqlVersion) instances on cluster nodes" 66 | $installResult = Install-DbaInstance -SqlInstance $sqlInstances -Version $instance.SqlVersion -Feature Engine ` 67 | -EngineCredential $SqlServerCredential -AgentCredential $SqlServerCredential ` 68 | -Path $SQLServerSourcesPath -UpdateSourcePath $SQLServerPatchesPath -Authentication Credssp -Credential $AdministratorCredential -Confirm:$false 69 | $installResult | Format-Table 70 | if ( $installResult.Notes -match 'restart' ) { 71 | Write-LocalHost -Message 'Restarting cluster nodes' 72 | Restart-Computer -ComputerName $ClusterNodes 73 | Start-Sleep -Seconds 120 74 | } 75 | 76 | Write-LocalHost -Message 'Configure SQL Server instances: MaxMemory / MaxDop / CostThresholdForParallelism' 77 | Set-DbaMaxMemory -SqlInstance $sqlInstances -Max 2048 | Format-Table 78 | Set-DbaMaxDop -SqlInstance $sqlInstances | Format-Table 79 | Set-DbaSpConfigure -SqlInstance $sqlInstances -Name CostThresholdForParallelism -Value 50 | Format-Table 80 | 81 | Write-LocalHost -Message 'Restore and configure demo database' 82 | $dbUpgradeNeeded = $false 83 | $dbVersion = $instance.SqlVersion 84 | if ( $dbVersion -eq '2019' ) { 85 | $dbVersion = '2017' 86 | $dbUpgradeNeeded = $true 87 | } 88 | Restore-DbaDatabase -SqlInstance $sqlInstances[0] -Path "$BackupPath\AdventureWorks$dbVersion.bak" -DatabaseName $DatabaseName | Out-Null 89 | $Database = Get-DbaDatabase -SqlInstance $sqlInstances[0] -Database $DatabaseName 90 | $Database.RecoveryModel = 'Full' 91 | $Database.Alter() 92 | if ( $dbUpgradeNeeded ) { 93 | Write-LocalHost -Message "Upgrade demo database" 94 | Invoke-DbaDbUpgrade -SqlInstance $sqlInstances[0] -Database $DatabaseName | Format-Table 95 | } 96 | $null = Backup-DbaDatabase -SqlInstance $sqlInstances[0] -Database $DatabaseName 97 | } 98 | 99 | Write-LocalHost -Message 'Grant instant file initialization rights to SQL Server service account on cluster nodes' 100 | Set-DbaPrivilege -ComputerName $ClusterNodes -Type IFI 101 | 102 | Write-LocalHost -Message 'finished' 103 | -------------------------------------------------------------------------------- /AlwaysOn_MultiInstance/02_setup_availability_group.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [string[]]$ClusterNodes = @('SRV1', 'SRV2'), 4 | [string[]]$InstanceNames = @('MSSQLSERVER', 'SQL2017', 'SQL2016', 'SQL2014'), 5 | [string[]]$SqlVersions = @('2019', '2017', '2016', '2014'), 6 | [int[]]$HadrEndpointPorts = @(5022, 5023, 5024, 5025), 7 | [string[]]$AvailabilityGroupNames = @('Adventure2019', 'Adventure2017', 'Adventure2016', 'Adventure2014'), 8 | [System.Net.IPAddress[]]$AvailabilityGroupIPs = @('192.168.3.71', '192.168.3.72', '192.168.3.73', '192.168.3.74'), 9 | [PSCredential]$AdministratorCredential = (New-Object -TypeName PSCredential -ArgumentList "COMPANY\Administrator", (ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force)), 10 | [PSCredential]$SqlServerCredential = (New-Object -TypeName PSCredential -ArgumentList "COMPANY\SQLServer", (ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force)), 11 | [string]$BackupPath = '\\WIN10\SQLServerBackups', 12 | [string]$DatabaseName = 'AdventureWorks' 13 | ) 14 | 15 | function Write-LocalWarning { 16 | param ( 17 | [string]$Message 18 | ) 19 | Write-Warning -Message ('{0}: {1}' -f (Get-Date), $Message) 20 | } 21 | 22 | function Write-LocalHost { 23 | param ( 24 | [string]$Message, 25 | [string]$ForegroundColor = 'Yellow' 26 | ) 27 | Microsoft.PowerShell.Utility\Write-Host -Object ('{0}: {1}' -f (Get-Date), $Message) -ForegroundColor $ForegroundColor 28 | } 29 | 30 | function Write-LocalVerbose { 31 | param ( 32 | [string]$Message 33 | ) 34 | Write-Verbose -Message ('{0}: {1}' -f (Get-Date), $Message) 35 | } 36 | 37 | $ErrorActionPreference = 'Stop' 38 | 39 | $instances = @() 40 | foreach ( $nr in 0 .. ($InstanceNames.Count - 1) ) { 41 | $instances += [PSCustomObject]@{ 42 | InstanceName = $InstanceNames[$nr] 43 | SqlVersion = $SqlVersions[$nr] 44 | HadrEndpointPort = $HadrEndpointPorts[$nr] 45 | AvailabilityGroupName = $AvailabilityGroupNames[$nr] 46 | AvailabilityGroupIP = $AvailabilityGroupIPs[$nr] 47 | } 48 | } 49 | 50 | Write-LocalHost -Message 'Import module dbatools' 51 | Import-Module -Name dbatools -MinimumVersion 1.0.124 52 | 53 | foreach ( $instance in $instances ) { 54 | $sqlInstances = @(); 55 | foreach ( $node in $ClusterNodes ) { 56 | $sqlInstances += "$node\$($instance.InstanceName)" 57 | } 58 | 59 | Write-LocalHost -Message "Configure SQL Server $($instance.SqlVersion) instance service to enable Always On" 60 | Enable-DbaAgHadr -SqlInstance $sqlInstances -Force | Format-Table 61 | 62 | Write-LocalHost -Message 'Configure and start extended event session AlwaysOn_health' 63 | Get-DbaXESession -SqlInstance $sqlInstances -Session AlwaysOn_health | ForEach-Object -Process { $_.AutoStart = $true ; $_.Alter() ; $_ | Start-DbaXESession } | Format-Table 64 | 65 | Write-LocalHost -Message 'Create endpoints' 66 | $endpoint = New-DbaEndpoint -SqlInstance $sqlInstances -Name hadr_endpoint -Type DatabaseMirroring -EndpointEncryption Supported -EncryptionAlgorithm Aes -Port $instance.HadrEndpointPort 67 | $endpoint | Start-DbaEndpoint | Format-Table 68 | 69 | $availabilityGroupParameters = @{ 70 | Primary = $SqlInstances[0] 71 | Secondary = $SqlInstances[1] 72 | Name = $instance.AvailabilityGroupName 73 | IPAddress = $instance.AvailabilityGroupIP 74 | Database = $DatabaseName 75 | ClusterType = 'Wsfc' 76 | Confirm = $false 77 | } 78 | Write-LocalHost -Message 'Create Always On Availability Group with manual seeding' 79 | New-DbaAvailabilityGroup @availabilityGroupParameters -SeedingMode Manual -SharedPath $BackupPath | Format-Table 80 | #New-DbaAvailabilityGroup @availabilityGroupParameters -SeedingMode Automatic | Format-Table 81 | Get-DbaAgReplica -SqlInstance $SqlInstances[0] -AvailabilityGroup $instance.AvailabilityGroupName | Format-Table 82 | Get-DbaAgDatabase -SqlInstance $SqlInstances -AvailabilityGroup $instance.AvailabilityGroupName -Database $DatabaseName | Format-Table 83 | 84 | } 85 | 86 | Write-LocalHost -Message 'finished' 87 | -------------------------------------------------------------------------------- /AlwaysOn_MultiInstance/02_setup_availability_group_demo_01_AgHadr_Endpoint_XESession.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Script to prepare every instance of SRV1 and SRV2 to build an availability group 3 | But step by step with only a little help from dbatools 4 | And with the choice to run some parts as plain SQL 5 | 6 | Run this script after: 01_setup_instances.ps1 7 | 8 | To update dbatools: Update-Module -Name dbatools 9 | To get the sql server error message: $Error[0].GetBaseException() 10 | #> 11 | 12 | # You can comment this out to use SQL instead when possible 13 | $useDBAtools = $true 14 | 15 | 16 | 17 | $ErrorActionPreference = 'Stop' 18 | 19 | Import-Module -Name dbatools -MinimumVersion 1.0.116 20 | 21 | $sqlInstance2014 = Connect-DbaInstance -SqlInstance 'SRV1\SQL2014', 'SRV2\SQL2014' 22 | $sqlInstance2016 = Connect-DbaInstance -SqlInstance 'SRV1\SQL2016', 'SRV2\SQL2016' 23 | $sqlInstance2017 = Connect-DbaInstance -SqlInstance 'SRV1\SQL2017', 'SRV2\SQL2017' 24 | $sqlInstance2019 = Connect-DbaInstance -SqlInstance 'SRV1\SQL2019', 'SRV2\SQL2019' 25 | 26 | $sqlInstanceAll = $sqlInstance2014 + $sqlInstance2016 + $sqlInstance2017 + $sqlInstance2019 27 | 28 | 29 | # We start with the prerequisites: 30 | # https://docs.microsoft.com/en-us/sql/database-engine/availability-groups/windows/prereqs-restrictions-recommendations-always-on-availability 31 | 32 | 33 | ####### 34 | # Task: Enable HADR on every instance 35 | ####### 36 | 37 | # https://docs.microsoft.com/en-us/sql/database-engine/availability-groups/windows/enable-and-disable-always-on-availability-groups-sql-server 38 | # To enable HADR, we will use dbatools, that works perfect: 39 | Enable-DbaAgHadr -SqlInstance $sqlInstanceAll -Force | Format-Table 40 | 41 | <# Output: 42 | 43 | ComputerName InstanceName SqlInstance IsHadrEnabled 44 | ------------ ------------ ----------- ------------- 45 | SRV1 SQL2014 SRV1\SQL2014 True 46 | SRV2 SQL2014 SRV2\SQL2014 True 47 | SRV1 SQL2016 SRV1\SQL2016 True 48 | SRV2 SQL2016 SRV2\SQL2016 True 49 | SRV1 SQL2017 SRV1\SQL2017 True 50 | SRV2 SQL2017 SRV2\SQL2017 True 51 | SRV1 SQL2019 SRV1\SQL2019 True 52 | SRV2 SQL2019 SRV2\SQL2019 True 53 | 54 | #> 55 | 56 | <# What does Enable-DbaAgHadr do? 57 | 58 | $sqlInstance = Connect-DbaInstance -SqlInstance 'SRV1\SQL2014' 59 | $computerName = $sqlInstance.ComputerName 60 | $instanceName = $sqlInstance.InstanceName 61 | $wmi = New-Object Microsoft.SqlServer.Management.Smo.Wmi.ManagedComputer $computerName 62 | $null = $wmi.Initialize() 63 | $sqlService = $wmi.Services | Where-Object DisplayName -EQ "SQL Server ($instanceName)" 64 | $sqlService.ChangeHadrServiceSetting(1) 65 | Invoke-Command -ComputerName $computerName -ScriptBlock { Restart-Service -DisplayName "SQL Server ($using:instanceName)" -Force } 66 | 67 | #> 68 | 69 | # There is no way to do this with SQL - only with Configuration Manager 70 | 71 | 72 | # Now we can create the availability groups 73 | 74 | # The wizard in SSMS can only create an availability group with one or more databases, but I will show here that we can build an empty availability group. 75 | # It will not be in healthy state untill we ad a database, but that's ok for me. 76 | 77 | # Here is the documentation for all the following steps for those who like to do it with SQL: 78 | # https://docs.microsoft.com/en-us/sql/database-engine/availability-groups/windows/create-an-availability-group-transact-sql 79 | 80 | # I will create all the endpoints in advance. Because I need different ports and because I want to show it step by step. 81 | # https://docs.microsoft.com/en-us/sql/t-sql/statements/create-endpoint-transact-sql 82 | 83 | # Differences between SSMS wizard and dbatools: 84 | # SSMS wizard uses "ENCRYPTION = REQUIRED", which is default to "CREATE ENDPOINT" and also used in the Microsoft documentation 85 | # dbatools uses "-EndpointEncryption Supported" when using "New-DbaEndpoint" inside of "Add-DbaAgReplica" 86 | # I will use "ENCRYPTION = REQUIRED" to follow the SSMS wizard 87 | 88 | # The service account is the same in all instances of the lab, so I have this fixed: 89 | $serviceAccount = 'COMPANY\SQLServer' 90 | 91 | 92 | ####### 93 | # Task: Create the endpoints and grant permissions to the service account 94 | ####### 95 | 96 | if ( $useDBAtools ) { 97 | New-DbaEndpoint -SqlInstance $sqlInstance2014 -Name hadr_endpoint -Type DatabaseMirroring -Protocol Tcp -Role All -EndpointEncryption Required -EncryptionAlgorithm Aes -Port 5022 | Start-DbaEndpoint | Format-Table 98 | New-DbaEndpoint -SqlInstance $sqlInstance2016 -Name hadr_endpoint -Type DatabaseMirroring -Protocol Tcp -Role All -EndpointEncryption Required -EncryptionAlgorithm Aes -Port 5023 | Start-DbaEndpoint | Format-Table 99 | New-DbaEndpoint -SqlInstance $sqlInstance2017 -Name hadr_endpoint -Type DatabaseMirroring -Protocol Tcp -Role All -EndpointEncryption Required -EncryptionAlgorithm Aes -Port 5024 | Start-DbaEndpoint | Format-Table 100 | New-DbaEndpoint -SqlInstance $sqlInstance2019 -Name hadr_endpoint -Type DatabaseMirroring -Protocol Tcp -Role All -EndpointEncryption Required -EncryptionAlgorithm Aes -Port 5025 | Start-DbaEndpoint | Format-Table 101 | 102 | New-DbaLogin -SqlInstance $sqlInstanceAll -Login $serviceAccount | Format-Table 103 | 104 | Grant-DbaAgPermission -SqlInstance $sqlInstanceAll -Login $serviceAccount -Type Endpoint -Permission Connect | Format-Table 105 | 106 | <# Output: 107 | 108 | ComputerName InstanceName SqlInstance ID Name Port EndpointState EndpointType Owner IsAdminEndpoint Fqdn IsSystemObject 109 | ------------ ------------ ----------- -- ---- ---- ------------- ------------ ----- --------------- ---- -------------- 110 | SRV1 SQL2014 SRV1\SQL2014 65536 hadr_endpoint 5022 Started DatabaseMirroring sa False TCP://srv1.Company.Pri:5022 False 111 | SRV2 SQL2014 SRV2\SQL2014 65536 hadr_endpoint 5022 Started DatabaseMirroring sa False TCP://srv2.Company.Pri:5022 False 112 | 113 | 114 | 115 | ComputerName InstanceName SqlInstance ID Name Port EndpointState EndpointType Owner IsAdminEndpoint Fqdn IsSystemObject 116 | ------------ ------------ ----------- -- ---- ---- ------------- ------------ ----- --------------- ---- -------------- 117 | SRV1 SQL2016 SRV1\SQL2016 65536 hadr_endpoint 5023 Started DatabaseMirroring sa False TCP://srv1.Company.Pri:5023 False 118 | SRV2 SQL2016 SRV2\SQL2016 65536 hadr_endpoint 5023 Started DatabaseMirroring sa False TCP://srv2.Company.Pri:5023 False 119 | 120 | 121 | 122 | ComputerName InstanceName SqlInstance ID Name Port EndpointState EndpointType Owner IsAdminEndpoint Fqdn IsSystemObject 123 | ------------ ------------ ----------- -- ---- ---- ------------- ------------ ----- --------------- ---- -------------- 124 | SRV1 SQL2017 SRV1\SQL2017 65536 hadr_endpoint 5024 Started DatabaseMirroring sa False TCP://srv1.Company.Pri:5024 False 125 | SRV2 SQL2017 SRV2\SQL2017 65536 hadr_endpoint 5024 Started DatabaseMirroring sa False TCP://SRV2.Company.Pri:5024 False 126 | 127 | 128 | 129 | ComputerName InstanceName SqlInstance ID Name Port EndpointState EndpointType Owner IsAdminEndpoint Fqdn IsSystemObject 130 | ------------ ------------ ----------- -- ---- ---- ------------- ------------ ----- --------------- ---- -------------- 131 | SRV1 SQL2019 SRV1\SQL2019 65536 hadr_endpoint 5025 Started DatabaseMirroring sa False TCP://srv1.Company.Pri:5025 False 132 | SRV2 SQL2019 SRV2\SQL2019 65536 hadr_endpoint 5025 Started DatabaseMirroring sa False TCP://SRV2.Company.Pri:5025 False 133 | 134 | 135 | 136 | ComputerName InstanceName SqlInstance Name LoginType CreateDate LastLogin HasAccess IsLocked IsDisabled 137 | ------------ ------------ ----------- ---- --------- ---------- --------- --------- -------- ---------- 138 | SRV1 SQL2014 SRV1\SQL2014 COMPANY\SQLServer WindowsUser 8/24/2020 7:17:45 PM 8/24/2020 6:57:45 PM True False 139 | SRV2 SQL2014 SRV2\SQL2014 COMPANY\SQLServer WindowsUser 8/24/2020 7:17:47 PM 8/24/2020 6:58:11 PM True False 140 | SRV1 SQL2016 SRV1\SQL2016 COMPANY\SQLServer WindowsUser 8/24/2020 7:17:47 PM 8/24/2020 6:58:38 PM True False 141 | SRV2 SQL2016 SRV2\SQL2016 COMPANY\SQLServer WindowsUser 8/24/2020 7:17:49 PM 8/24/2020 6:58:53 PM True False 142 | SRV1 SQL2017 SRV1\SQL2017 COMPANY\SQLServer WindowsUser 8/24/2020 7:17:49 PM 8/24/2020 6:59:12 PM True False 143 | SRV2 SQL2017 SRV2\SQL2017 COMPANY\SQLServer WindowsUser 8/24/2020 7:17:51 PM 8/24/2020 6:59:26 PM True False 144 | SRV1 SQL2019 SRV1\SQL2019 COMPANY\SQLServer WindowsUser 8/24/2020 7:17:51 PM 8/24/2020 6:59:45 PM True False 145 | SRV2 SQL2019 SRV2\SQL2019 COMPANY\SQLServer WindowsUser 8/24/2020 7:17:53 PM 8/24/2020 7:00:05 PM True False 146 | 147 | 148 | 149 | ComputerName InstanceName SqlInstance Name Permission Type Status 150 | ------------ ------------ ----------- ---- ---------- ---- ------ 151 | SRV1 SQL2014 SRV1\SQL2014 COMPANY\SQLServer Connect Grant Success 152 | SRV2 SQL2014 SRV2\SQL2014 COMPANY\SQLServer Connect Grant Success 153 | SRV1 SQL2016 SRV1\SQL2016 COMPANY\SQLServer Connect Grant Success 154 | SRV2 SQL2016 SRV2\SQL2016 COMPANY\SQLServer Connect Grant Success 155 | SRV1 SQL2017 SRV1\SQL2017 COMPANY\SQLServer Connect Grant Success 156 | SRV2 SQL2017 SRV2\SQL2017 COMPANY\SQLServer Connect Grant Success 157 | SRV1 SQL2019 SRV1\SQL2019 COMPANY\SQLServer Connect Grant Success 158 | SRV2 SQL2019 SRV2\SQL2019 COMPANY\SQLServer Connect Grant Success 159 | 160 | #> 161 | 162 | 163 | } else { 164 | # How to do it per SQL? 165 | 166 | foreach ( $instance in $sqlInstance2014 ) { 167 | $instance.Query("CREATE ENDPOINT [hadr_endpoint] STATE = STARTED AS TCP (LISTENER_PORT = 5022) FOR DATA_MIRRORING (ROLE = ALL, ENCRYPTION = REQUIRED ALGORITHM AES)") 168 | $instance.Query("CREATE LOGIN [$serviceAccount] FROM WINDOWS") 169 | $instance.Query("GRANT CONNECT ON ENDPOINT::[hadr_endpoint] TO [$serviceAccount]") 170 | } 171 | 172 | foreach ( $instance in $sqlInstance2016 ) { 173 | $instance.Query("CREATE ENDPOINT [hadr_endpoint] STATE = STARTED AS TCP (LISTENER_PORT = 5023) FOR DATA_MIRRORING (ROLE = ALL, ENCRYPTION = REQUIRED ALGORITHM AES)") 174 | $instance.Query("CREATE LOGIN [$serviceAccount] FROM WINDOWS") 175 | $instance.Query("GRANT CONNECT ON ENDPOINT::[hadr_endpoint] TO [$serviceAccount]") 176 | } 177 | 178 | foreach ( $instance in $sqlInstance2017 ) { 179 | $instance.Query("CREATE ENDPOINT [hadr_endpoint] STATE = STARTED AS TCP (LISTENER_PORT = 5024) FOR DATA_MIRRORING (ROLE = ALL, ENCRYPTION = REQUIRED ALGORITHM AES)") 180 | $instance.Query("CREATE LOGIN [$serviceAccount] FROM WINDOWS") 181 | $instance.Query("GRANT CONNECT ON ENDPOINT::[hadr_endpoint] TO [$serviceAccount]") 182 | } 183 | 184 | foreach ( $instance in $sqlInstance2019 ) { 185 | $instance.Query("CREATE ENDPOINT [hadr_endpoint] STATE = STARTED AS TCP (LISTENER_PORT = 5025) FOR DATA_MIRRORING (ROLE = ALL, ENCRYPTION = REQUIRED ALGORITHM AES)") 186 | $instance.Query("CREATE LOGIN [$serviceAccount] FROM WINDOWS") 187 | $instance.Query("GRANT CONNECT ON ENDPOINT::[hadr_endpoint] TO [$serviceAccount]") 188 | } 189 | } 190 | 191 | 192 | 193 | ####### 194 | # Task: Configure and start the AlwaysOn_health extended event session 195 | ####### 196 | 197 | # This is not done by the dbatools command New-DbaAvailabilityGroup, but it is done by the SSMS wizard - I will do it here 198 | 199 | # https://docs.microsoft.com/en-us/sql/database-engine/availability-groups/windows/always-on-extended-events 200 | # https://www.sqlservercentral.com/blogs/alwayson_health-extended-event-session 201 | # https://www.mssqltips.com/sqlservertip/5287/monitoring-sql-server-availability-groups-with-alwayson-extended-events-health-session/ 202 | 203 | if ( $useDBAtools ) { 204 | Get-DbaXESession -SqlInstance $sqlInstanceAll -Session AlwaysOn_health | ForEach-Object -Process { $_.AutoStart = $true ; $_.Alter() ; $_ | Start-DbaXESession } | Format-Table 205 | 206 | <# Output: 207 | 208 | ComputerName InstanceName SqlInstance Name Status StartTime AutoStart State Targets TargetFile Events 209 | ------------ ------------ ----------- ---- ------ --------- --------- ----- ------- ---------- ------ 210 | SRV1 SQL2014 SRV1\SQL2014 AlwaysOn_health Running 8/24/2020 7:23:11 PM True Existing {package0.event_file} {C:\Program Files\Microsoft SQL Server\MSSQL12.SQL2014\MSSQL\Log\AlwaysOn_health.xel} {sqlserver.alwayson_ddl_executed, sqlserver... 211 | SRV2 SQL2014 SRV2\SQL2014 AlwaysOn_health Running 8/24/2020 7:23:14 PM True Existing {package0.event_file} {C:\Program Files\Microsoft SQL Server\MSSQL12.SQL2014\MSSQL\Log\AlwaysOn_health.xel} {sqlserver.alwayson_ddl_executed, sqlserver... 212 | SRV1 SQL2016 SRV1\SQL2016 AlwaysOn_health Running 8/24/2020 7:23:16 PM True Existing {package0.event_file} {C:\Program Files\Microsoft SQL Server\MSSQL13.SQL2016\MSSQL\Log\AlwaysOn_health.xel} {sqlserver.alwayson_ddl_executed, sqlserver... 213 | SRV2 SQL2016 SRV2\SQL2016 AlwaysOn_health Running 8/24/2020 7:23:24 PM True Existing {package0.event_file} {C:\Program Files\Microsoft SQL Server\MSSQL13.SQL2016\MSSQL\Log\AlwaysOn_health.xel} {sqlserver.alwayson_ddl_executed, sqlserver... 214 | SRV1 SQL2017 SRV1\SQL2017 AlwaysOn_health Running 8/24/2020 7:23:25 PM True Existing {package0.event_file} {C:\Program Files\Microsoft SQL Server\MSSQL14.SQL2017\MSSQL\Log\AlwaysOn_health.xel} {sqlserver.alwayson_ddl_executed, sqlserver... 215 | SRV2 SQL2017 SRV2\SQL2017 AlwaysOn_health Running 8/24/2020 7:23:50 PM True Existing {package0.event_file} {C:\Program Files\Microsoft SQL Server\MSSQL14.SQL2017\MSSQL\Log\AlwaysOn_health.xel} {sqlserver.alwayson_ddl_executed, sqlserver... 216 | SRV1 SQL2019 SRV1\SQL2019 AlwaysOn_health Running 8/24/2020 7:23:51 PM True Existing {package0.event_file} {C:\Program Files\Microsoft SQL Server\MSSQL15.SQL2019\MSSQL\Log\AlwaysOn_health.xel} {sqlserver.alwayson_ddl_executed, sqlserver... 217 | SRV2 SQL2019 SRV2\SQL2019 AlwaysOn_health Running 8/24/2020 7:23:53 PM True Existing {package0.event_file} {C:\Program Files\Microsoft SQL Server\MSSQL15.SQL2019\MSSQL\Log\AlwaysOn_health.xel} {sqlserver.alwayson_ddl_executed, sqlserver... 218 | 219 | #> 220 | 221 | 222 | <# What does Get-DbaXESession do? 223 | 224 | $smoServer = $sqlInstance2014[0] 225 | $sqlConn = $smoServer.ConnectionContext.SqlConnectionObject 226 | $sqlStoreConnection = New-Object Microsoft.SqlServer.Management.Sdk.Sfc.SqlStoreConnection $sqlConn 227 | $xeStore = New-Object Microsoft.SqlServer.Management.XEvent.XEStore $sqlStoreConnection 228 | $xeSession = $xeStore.sessions | Where-Object Name -EQ 'AlwaysOn_health' 229 | $xeSession.AutoStart = $true 230 | $xeSession.Alter() 231 | $xeSession.Start() 232 | 233 | #> 234 | 235 | } else { 236 | # How to do it per SQL? 237 | 238 | foreach ( $instance in $sqlInstanceAll ) { 239 | $instance.Query("ALTER EVENT SESSION [AlwaysOn_health] ON SERVER WITH (STARTUP_STATE = ON)") 240 | $instance.Query("ALTER EVENT SESSION [AlwaysOn_health] ON SERVER STATE = START") 241 | } 242 | } 243 | 244 | -------------------------------------------------------------------------------- /AlwaysOn_MultiInstance/03_add_SRV3_to_Cluster_and_AvailabilityGroup.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [string]$DomainName = 'COMPANY', 4 | [string]$DomainController = 'DOM1', 5 | [string]$NewClusterNode = 'SRV3', 6 | [string]$ClusterName = 'SQLCluster' 7 | ) 8 | 9 | function Write-LocalWarning { 10 | param ( 11 | [string]$Message 12 | ) 13 | Write-Warning -Message ('{0}: {1}' -f (Get-Date), $Message) 14 | } 15 | 16 | function Write-LocalHost { 17 | param ( 18 | [string]$Message, 19 | [string]$ForegroundColor = 'Yellow' 20 | ) 21 | Microsoft.PowerShell.Utility\Write-Host -Object ('{0}: {1}' -f (Get-Date), $Message) -ForegroundColor $ForegroundColor 22 | } 23 | 24 | function Write-LocalVerbose { 25 | param ( 26 | [string]$Message 27 | ) 28 | Write-Verbose -Message ('{0}: {1}' -f (Get-Date), $Message) 29 | } 30 | 31 | $ErrorActionPreference = 'Stop' 32 | 33 | Write-LocalHost -Message 'Install cluster freature on new node' 34 | $installResult = Invoke-Command -ComputerName $NewClusterNode -ScriptBlock { Install-WindowsFeature -Name Failover-Clustering -IncludeManagementTools } 35 | $installResult | Format-Table 36 | if ( $installResult.RestartNeeded -eq 'Yes' ) { 37 | # Restart is needed on Windows Server 2019 38 | Restart-Computer -ComputerName $NewClusterNode 39 | Start-Sleep -Seconds 60 40 | } 41 | 42 | Write-LocalHost -Message 'Install cluster freature on new node' 43 | Get-Cluster -Name $ClusterName | Add-ClusterNode -Name $NewClusterNode 44 | # This should output something, but it does not 45 | 46 | Write-LocalHost -Message 'Lets see if we have a new node' 47 | Get-Cluster -Name $ClusterName | Get-ClusterNode 48 | # Yes, we have 49 | 50 | 51 | # put code in here from: SRV3_MultiInstance\setup_instances.ps1 52 | 53 | 54 | # plan: 55 | 56 | # Add replica (delete database AdventureWorks befor that - or not?) 57 | # Add databases (maybe they are added automatically with automatic seeding? 58 | 59 | 60 | Write-LocalHost -Message 'finished' 61 | -------------------------------------------------------------------------------- /AutomatedLab/01_Install_HyperV.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Stop' 2 | 3 | # Change execution policy to remote signed if needed: 4 | ##################################################### 5 | 6 | $currentPolicy = Get-ExecutionPolicy 7 | if ($currentPolicy -ne 'RemoteSigned') { 8 | Write-Warning -Message "ExecutionPolicy is currently $currentPolicy, so we change it to RemoteSigned" 9 | Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Force 10 | } 11 | 12 | <# Just for information: 13 | 14 | Get-ExecutionPolicy -List 15 | 16 | Scope ExecutionPolicy 17 | ----- --------------- 18 | MachinePolicy Undefined 19 | UserPolicy Undefined 20 | Process Undefined 21 | CurrentUser Undefined 22 | LocalMachine RemoteSigned 23 | 24 | #> 25 | 26 | 27 | # Enable Hyper-V (restart needed): 28 | ################################## 29 | 30 | $currentHyperV = Get-WindowsOptionalFeature -FeatureName Microsoft-Hyper-V -Online 31 | if ($currentHyperV.State -ne 'Enabled') { 32 | Write-Warning -Message "State of Microsoft-Hyper-V is currently $($currentHyperV.State), so we enable it and restart the computer" 33 | Enable-WindowsOptionalFeature -FeatureName Microsoft-Hyper-V -All -Online -NoRestart 34 | Restart-Computer -Force 35 | } 36 | -------------------------------------------------------------------------------- /AutomatedLab/02_Install_AutomatedLab.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Stop' 2 | 3 | # Setup PSGallery as a trusted source for PowerShell modules: 4 | ############################################################# 5 | 6 | # Maybe needed for older systems that don't use TLS 1.2: 7 | # [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 8 | $null = Install-PackageProvider -Name Nuget -Force 9 | Set-PSRepository -Name PSGallery -InstallationPolicy Trusted 10 | 11 | 12 | # Install and configure the PowerShell module AutomatedLab: 13 | ########################################################### 14 | 15 | Install-Module -Name AutomatedLab -AllowClobber -SkipPublisherCheck -Force 16 | Install-Module -Name Posh-SSH 17 | 18 | [Environment]::SetEnvironmentVariable('AUTOMATEDLAB_TELEMETRY_OPTIN', 'false', 'Machine') 19 | $env:AUTOMATEDLAB_TELEMETRY_OPTIN = 'false' 20 | 21 | Set-PSFConfig -Module AutomatedLab -Name LabSourcesLocation -Description 'Location of lab sources folder' -Validation string -Value 'C:\AutomatedLab-Sources' -PassThru | Register-PSFConfig 22 | Set-PSFConfig -Module AutomatedLab -Name VmPath -Description 'Location of lab vm folder' -Validation string -Value 'C:\AutomatedLab-VMs' -PassThru | Register-PSFConfig 23 | 24 | Import-Module -Name AutomatedLab 25 | 26 | New-LabSourcesFolder 27 | 28 | Enable-LabHostRemoting -Force 29 | 30 | 31 | # Download some operating system ISOs: 32 | ###################################### 33 | 34 | $isoList = @( 35 | @{ 36 | Uri = 'https://software-static.download.prss.microsoft.com/sg/download/888969d5-f34g-4e03-ac9d-1f9786c66749/SERVER_EVAL_x64FRE_en-us.iso' 37 | OutFile = "$labSources\ISOs\2022_x64_EN_Eval.iso" 38 | } 39 | <# 40 | @{ 41 | Uri = 'https://software-download.microsoft.com/download/sg/17763.253.190108-0006.rs5_release_svc_refresh_SERVER_EVAL_x64FRE_en-us.iso' 42 | OutFile = "$labSources\ISOs\2019_x64_EN_Eval.iso" 43 | } 44 | @{ 45 | Uri = 'https://software-static.download.prss.microsoft.com/dbazure/988969d5-f34g-4e03-ac9d-1f9786c66751/22621.525.220925-0207.ni_release_svc_refresh_CLIENTENTERPRISEEVAL_OEMRET_x64FRE_en-us.iso' 46 | OutFile = "$labSources\ISOs\WIN11_x64_ENT_22H2_EN_Eval.iso" 47 | } 48 | @{ 49 | Uri = 'https://software-download.microsoft.com/download/sg/444969d5-f34g-4e03-ac9d-1f9786c69161/19044.1288.211006-0501.21h2_release_svc_refresh_CLIENTENTERPRISEEVAL_OEMRET_x64FRE_en-us.iso' 50 | OutFile = "$labSources\ISOs\WIN10_x64_ENT_21H2_EN_Eval.iso" 51 | } 52 | #> 53 | @{ 54 | Uri = 'https://mirror1.hs-esslingen.de/pub/Mirrors/centos/7.9.2009/isos/x86_64/CentOS-7-x86_64-DVD-2207-02.iso' 55 | OutFile = "$labSources\ISOs\CentOS-7-x86_64-DVD-2207-02.iso" 56 | } 57 | <# 58 | @{ 59 | Uri = 'https://mirror1.hs-esslingen.de/pub/Mirrors/centos-stream/9-stream/BaseOS/x86_64/iso/CentOS-Stream-9-latest-x86_64-dvd1.iso' 60 | OutFile = "$labSources\ISOs\CentOS-Stream-9-latest-x86_64-dvd1.iso" 61 | } 62 | @{ 63 | Uri = 'https://download.opensuse.org/distribution/leap/15.2/iso/openSUSE-Leap-15.2-DVD-x86_64.iso' 64 | OutFile = "$labSources\ISOs\openSUSE-Leap-15.2-DVD-x86_64.iso" 65 | } 66 | #> 67 | ) 68 | # You can find more windows operating systems here: https://github.com/VirtualEngine/Lability/blob/dev/Config/Media.json 69 | # The link for Windows 11 is from here: https://github.com/VirtualEngine/Lability/pull/415/files 70 | 71 | foreach ($iso in $isoList) { 72 | if (-not (Test-Path -Path $iso.OutFile)) { 73 | Invoke-WebRequest -Uri $iso.Uri -OutFile $iso.OutFile -UseBasicParsing 74 | } 75 | } 76 | 77 | Get-LabAvailableOperatingSystem | Sort-Object OperatingSystemName 78 | 79 | <# 80 | 81 | OperatingSystemName Idx Version PublishedDate IsoPath 82 | ------------------- --- ------- ------------- ------- 83 | CentOS Stream 9 0 9.0 12.12.2022 11:04:22 C:\AutomatedLab-Sources\ISOs\CentOS-Stream-9-latest-x86_64-... 84 | CentOS-7 0 7.0 26.07.2022 16:40:33 C:\AutomatedLab-Sources\ISOs\CentOS-7-x86_64-DVD-2207-02.iso 85 | openSUSE Leap 15.2 0 15.0 09.06.2020 17:18:51 C:\AutomatedLab-Sources\ISOs\openSUSE-Leap-15.2-DVD-x86_64.iso 86 | Windows 10 Enterprise Evaluation 1 10.0.19041.1288 06.10.2021 15:07:52 C:\AutomatedLab-Sources\ISOs\WIN10_x64_ENT_21H2_EN_Eval.iso 87 | Windows 11 Enterprise Evaluation 1 10.0.22621.525 25.09.2022 03:47:56 C:\AutomatedLab-Sources\ISOs\WIN11_x64_ENT_22H2_EN_Eval.iso 88 | Windows Server 2019 Datacenter Evaluation 3 10.0.17763.253 08.01.2019 02:26:26 C:\AutomatedLab-Sources\ISOs\2019_x64_EN_Eval.iso 89 | Windows Server 2019 Datacenter Evaluation (Desktop Experience) 4 10.0.17763.253 08.01.2019 02:27:56 C:\AutomatedLab-Sources\ISOs\2019_x64_EN_Eval.iso 90 | Windows Server 2019 Standard Evaluation 1 10.0.17763.253 08.01.2019 02:29:22 C:\AutomatedLab-Sources\ISOs\2019_x64_EN_Eval.iso 91 | Windows Server 2019 Standard Evaluation (Desktop Experience) 2 10.0.17763.253 08.01.2019 02:27:51 C:\AutomatedLab-Sources\ISOs\2019_x64_EN_Eval.iso 92 | Windows Server 2022 Datacenter Evaluation 3 10.0.20348.587 03.03.2022 05:03:12 C:\AutomatedLab-Sources\ISOs\2022_x64_EN_Eval.iso 93 | Windows Server 2022 Datacenter Evaluation (Desktop Experience) 4 10.0.20348.587 03.03.2022 05:10:29 C:\AutomatedLab-Sources\ISOs\2022_x64_EN_Eval.iso 94 | Windows Server 2022 Standard Evaluation 1 10.0.20348.587 03.03.2022 05:02:13 C:\AutomatedLab-Sources\ISOs\2022_x64_EN_Eval.iso 95 | Windows Server 2022 Standard Evaluation (Desktop Experience) 2 10.0.20348.587 03.03.2022 05:08:50 C:\AutomatedLab-Sources\ISOs\2022_x64_EN_Eval.iso 96 | 97 | #> 98 | -------------------------------------------------------------------------------- /AutomatedLab/03_Setup_TestLab.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Continue' 2 | 3 | Import-Module -Name AutomatedLab 4 | Import-Module -Name Posh-SSH 5 | 6 | $LabName = 'TestLab' 7 | $LabNetworkBase = '192.168.123' 8 | 9 | $LabAdminUser = 'User' 10 | $LabAdminPassword = 'Passw0rd!' 11 | 12 | # If you disable the following line, only Windows and Linux will be deployed (which is a faster test) 13 | $LabDomainName = 'testlab.local' 14 | 15 | # If you disable the following line, we try to retrieve the currently used dns server address 16 | #$LabDnsServer = '1.1.1.1' 17 | 18 | 19 | $MachineDefinitionDefaults = @{ 20 | Processors = 2 21 | Memory = 2GB 22 | Network = $LabName 23 | Gateway = "$LabNetworkBase.1" 24 | TimeZone = 'W. Europe Standard Time' 25 | } 26 | 27 | $MachineDefinition = @( 28 | @{ 29 | Name = 'DC' 30 | OperatingSystem = 'Windows Server 2022 Standard Evaluation (Desktop Experience)' 31 | IpAddress = "$LabNetworkBase.10" 32 | Roles = 'RootDC' 33 | } 34 | @{ 35 | Name = 'Windows' 36 | OperatingSystem = 'Windows Server 2022 Standard Evaluation (Desktop Experience)' 37 | IpAddress = "$LabNetworkBase.20" 38 | } 39 | @{ 40 | Name = 'Linux' 41 | OperatingSystem = 'CentOS-7' 42 | IpAddress = "$LabNetworkBase.30" 43 | } 44 | ) 45 | 46 | 47 | $ChocolateyPackages = @( 48 | 'powershell-core' 49 | 'notepadplusplus' 50 | '7zip' 51 | 'vscode' 52 | 'vscode-powershell' 53 | 'googlechrome' 54 | ) 55 | 56 | $PowerShellModules = @( 57 | 'PSFramework' 58 | 'ImportExcel' 59 | 'Posh-SSH' 60 | 'dbatools' 61 | ) 62 | 63 | $DockerRunCommands = @( 64 | # "docker run --name SQLServer --memory=2g -p 1433:1433 -e MSSQL_SA_PASSWORD='$LabAdminPassword' -e ACCEPT_EULA=Y -e MSSQL_PID=Express --detach --restart always mcr.microsoft.com/mssql/server:2019-latest" 65 | # "docker run --name Oracle --memory=3g -p 1521:1521 -e ORACLE_PWD='$LabAdminPassword' -e ORACLE_CHARACTERSET=AL32UTF8 --detach --restart always container-registry.oracle.com/database/express:latest" 66 | "docker run --name MySQL --memory=1g -p 3306:3306 -e MYSQL_ROOT_PASSWORD='$LabAdminPassword' --detach --restart always mysql:latest" 67 | # As an alternative for MySQL: 68 | # "docker run --name MariaDB --memory=1g -p 3306:3306 -e MARIADB_ROOT_PASSWORD='$LabAdminPassword' --detach --restart always mariadb:latest" 69 | # "docker run --name PostgreSQL --memory=1g -p 5432:5432 -e POSTGRES_PASSWORD='$LabAdminPassword' --detach --restart always postgres:latest" 70 | # As an alternative for PostgreSQL: 71 | # "docker run --name PostGIS --memory=1g -p 5432:5432 -e POSTGRES_PASSWORD='$LabAdminPassword' --detach --restart always postgres:latest" 72 | ) 73 | 74 | 75 | ### End of configuration ### 76 | 77 | 78 | Set-PSFConfig -Module AutomatedLab -Name DoNotWaitForLinux -Value $true 79 | 80 | New-LabDefinition -Name $LabName -DefaultVirtualizationEngine HyperV 81 | Set-LabInstallationCredential -Username $LabAdminUser -Password $LabAdminPassword 82 | if ($LabDomainName) { 83 | Add-LabDomainDefinition -Name $LabDomainName -AdminUser $LabAdminUser -AdminPassword $LabAdminPassword 84 | $MachineDefinitionDefaults.DomainName = $LabDomainName 85 | } else { 86 | $MachineDefinition = $MachineDefinition | Where-Object Roles -NotContains RootDC 87 | if ($LabDnsServer) { 88 | $dnsServer1 = $LabDnsServer 89 | } else { 90 | $dnsServer1 = Get-NetAdapter | 91 | Where-Object { $_.Status -eq 'Up' -and $_.Name -notlike 'vEthernet*' } | 92 | Get-DnsClientServerAddress -AddressFamily IPv4 | 93 | Select-Object -ExpandProperty ServerAddresses -First 1 94 | } 95 | $MachineDefinitionDefaults.DnsServer1 = $dnsServer1 96 | } 97 | Add-LabVirtualNetworkDefinition -Name $LabName -AddressSpace "$LabNetworkBase.0/24" 98 | foreach ($md in $MachineDefinition) { 99 | # $md = $MachineDefinition[0] 100 | $lmd = @{ } 101 | foreach ($key in $MachineDefinitionDefaults.Keys) { 102 | $lmd.$key = $MachineDefinitionDefaults.$key 103 | } 104 | foreach ($key in $md.Keys) { 105 | $lmd.$key = $md.$key 106 | } 107 | $lmd.ResourceName = "$LabName-$($md.Name)" 108 | Add-LabMachineDefinition @lmd 109 | } 110 | Install-Lab -NoValidation 111 | 112 | $null = New-NetNat -Name $LabName -InternalIPInterfaceAddressPrefix "$LabNetworkBase.0/24" 113 | 114 | Start-LabVM -ComputerName Linux 115 | 116 | Invoke-LabCommand -ComputerName (Get-LabVM) -ActivityName 'Disable Windows updates' -ScriptBlock { 117 | # https://learn.microsoft.com/en-us/windows/deployment/update/waas-wu-settings 118 | Set-ItemProperty -Path HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU -Name NoAutoUpdate -Value 1 119 | } 120 | 121 | Invoke-LabCommand -ComputerName (Get-LabVM) -ActivityName 'Setting my favorite explorer settings' -ScriptBlock { 122 | Set-ItemProperty -Path HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced -Name HideFileExt -Value 0 123 | Set-ItemProperty -Path HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced -Name NavPaneShowAllFolders -Value 1 124 | Set-ItemProperty -Path HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced -Name NavPaneExpandToCurrentFolder -Value 1 125 | } 126 | 127 | $pingSucceeded = Invoke-LabCommand -ComputerName Windows -ActivityName 'Testing internet access' -PassThru -ScriptBlock { 128 | (Test-NetConnection -ComputerName www.google.de -WarningAction SilentlyContinue).PingSucceeded 129 | } 130 | 131 | if (-not $pingSucceeded) { 132 | Write-Warning -Message "We don't have internet access, but let's wait for 30 seconds and try again" 133 | Start-Sleep -Seconds 30 134 | $pingSucceeded = Invoke-LabCommand -ComputerName Windows -ActivityName 'Testing internet access' -PassThru -ScriptBlock { 135 | (Test-NetConnection -ComputerName www.google.de -WarningAction SilentlyContinue).PingSucceeded 136 | } 137 | if (-not $pingSucceeded) { 138 | Write-Warning -Message "We don't have internet access, so stopping here" 139 | break 140 | } 141 | } 142 | 143 | Invoke-LabCommand -ComputerName Windows -ActivityName 'Installing chocolatey packages' -ArgumentList @(, $ChocolateyPackages) -ScriptBlock { 144 | param($ChocolateyPackages) 145 | 146 | $ErrorActionPreference = 'Stop' 147 | 148 | $logPath = 'C:\DeployDebug\InstallChocolateyPackages.log' 149 | 150 | try { 151 | Invoke-Expression -Command ([System.Net.WebClient]::new().DownloadString('https://chocolatey.org/install.ps1')) *>$logPath 152 | $installResult = choco install $ChocolateyPackages --confirm --limitoutput --no-progress *>&1 153 | if ($installResult -match 'Warnings:') { 154 | Write-Warning -Message 'Chocolatey generated warnings' 155 | } 156 | $info = $installResult -match 'Chocolatey installed (\d+)/(\d+) packages' | Select-Object -First 1 157 | if ($info -match 'Chocolatey installed (\d+)/(\d+) packages') { 158 | if ($Matches[1] -ne $Matches[2]) { 159 | Write-Warning -Message "Chocolatey only installed $($Matches[1]) of $($Matches[2]) packages" 160 | $installResult | Add-Content -Path $logPath 161 | } 162 | } else { 163 | Write-Warning -Message "InstallResult: $installResult" 164 | } 165 | } catch { 166 | $message = "Setting up Chocolatey failed: $_" 167 | $message | Add-Content -Path $logPath 168 | Write-Warning -Message $message 169 | } 170 | } 171 | 172 | Invoke-LabCommand -ComputerName Windows -ActivityName 'Installing PowerShell modules' -ArgumentList @(, $PowerShellModules) -ScriptBlock { 173 | param($PowerShellModules) 174 | 175 | $logPath = 'C:\DeployDebug\InstallPowerShellModules.log' 176 | 177 | $ErrorActionPreference = 'Stop' 178 | 179 | try { 180 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 181 | if ((Get-PackageProvider -ListAvailable).Name -notcontains 'Nuget') { 182 | $null = Install-PackageProvider -Name Nuget -Force 183 | 'Install-PackageProvider ok' | Add-Content -Path $logPath 184 | } else { 185 | 'Install-PackageProvider not needed' | Add-Content -Path $logPath 186 | } 187 | if ((Get-PSRepository -Name PSGallery).InstallationPolicy -ne 'Trusted') { 188 | Set-PSRepository -Name PSGallery -InstallationPolicy Trusted 189 | 'Set-PSRepository ok' | Add-Content -Path $logPath 190 | } else { 191 | 'Set-PSRepository not needed' | Add-Content -Path $logPath 192 | } 193 | foreach ($name in $PowerShellModules) { 194 | if (-not (Get-Module -Name $name -ListAvailable)) { 195 | Install-Module -Name $name 196 | "Install-Module $name ok" | Add-Content -Path $logPath 197 | } else { 198 | "Install-Module $name not needed" | Add-Content -Path $logPath 199 | } 200 | } 201 | } catch { 202 | $message = "Setting up PowerShell failed: $_" 203 | $message | Add-Content -Path $logPath 204 | Write-Warning -Message $message 205 | } 206 | } 207 | 208 | Invoke-LabCommand -ComputerName Windows -ActivityName 'Downloading PowerShell-for-DBAs' -ScriptBlock { 209 | $logPath = 'C:\DeployDebug\InstallPowerShell-for-DBAs.log' 210 | 211 | $ErrorActionPreference = 'Stop' 212 | 213 | try { 214 | $null = New-Item -Path C:\GitHub -ItemType Directory 215 | 216 | Invoke-WebRequest -Uri https://github.com/andreasjordan/PowerShell-for-DBAs/archive/refs/heads/main.zip -OutFile C:\GitHub\main.zip -UseBasicParsing 217 | Expand-Archive -Path C:\GitHub\main.zip -DestinationPath C:\GitHub 218 | Rename-Item C:\GitHub\PowerShell-for-DBAs-main -NewName PowerShell-for-DBAs 219 | Remove-Item C:\GitHub\main.zip 220 | 221 | $null = New-Item -Path C:\NuGet -ItemType Directory 222 | foreach ($package in 'Oracle.ManagedDataAccess', 'Oracle.ManagedDataAccess.Core', 'MySql.Data', 'Npgsql', 'Microsoft.Extensions.Logging.Abstractions') { 223 | Invoke-WebRequest -Uri https://www.nuget.org/api/v2/package/$package -OutFile C:\NuGet\package.zip -UseBasicParsing 224 | Expand-Archive -Path C:\NuGet\package.zip -DestinationPath C:\NuGet\$package 225 | Remove-Item -Path C:\NuGet\package.zip 226 | } 227 | } catch { 228 | $message = "Setting up files for PowerShell-for-DBAs failed: $_" 229 | $message | Add-Content -Path $logPath 230 | Write-Warning -Message $message 231 | } 232 | } 233 | 234 | Write-PSFMessage -Level Host -Message "Setup of Windows finished, now we wait for Linux to be reachable" 235 | $rootCredential = [PSCredential]::new('root', (ConvertTo-SecureString -String $LabAdminPassword -AsPlainText -Force)) 236 | $linuxIp = ($MachineDefinition | Where-Object Name -eq Linux).IpAddress 237 | while ($true) { 238 | try { 239 | $sshSession = New-SSHSession -ComputerName $linuxIp -Credential $rootCredential -Force -WarningAction SilentlyContinue -ErrorAction Stop 240 | break 241 | } catch { 242 | Start-Sleep -Seconds 10 243 | } 244 | } 245 | 246 | Write-PSFMessage -Level Host -Message "Installing docker on Linux" 247 | $sshCommands = @( 248 | 'yum -y update' 249 | 'yum install -y yum-utils' 250 | 'yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo' 251 | 'yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin' 252 | 'systemctl enable --now docker' 253 | ) 254 | foreach ($cmd in $sshCommands) { 255 | $sshResult = Invoke-SSHCommand -SSHSession $sshSession -Command $cmd -TimeOut 600 256 | if ($sshResult.ExitStatus -gt 0) { 257 | Write-PSFMessage -Level Warning -Message "Command '$cmd' returned with ExitStatus $($sshResult.ExitStatus)" 258 | break 259 | } 260 | } 261 | 262 | Write-PSFMessage -Level Host -Message "Starting databases on docker" 263 | foreach ($cmd in $DockerRunCommands) { 264 | $containerName = $cmd -replace '^.*--name ([^ ]+).*$', '$1' 265 | Write-PSFMessage -Level Host -Message "Starting docker container $containerName" 266 | $null = Invoke-SSHCommand -SSHSession $sshSession -Command $cmd -TimeOut 36000 267 | } 268 | 269 | Write-PSFMessage -Level Host -Message "finished" 270 | 271 | 272 | break 273 | 274 | 275 | vmconnect.exe localhost $LabName-Windows 276 | 277 | Remove-Lab -Name $LabName -Confirm:$false ; Get-NetNat -Name $LabName -ErrorAction SilentlyContinue | Remove-NetNat -Confirm:$false 278 | 279 | -------------------------------------------------------------------------------- /AutomatedLab/04_Download_CustomAssets.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Stop' 2 | 3 | Import-Module -Name AutomatedLab 4 | 5 | # Download the GitHub repository and move CustomAssets and CustomScripts to LabSources directory: 6 | ################################################################################################# 7 | 8 | $tmpFolder = Join-Path -Path $env:TEMP -ChildPath (New-Guid).Guid 9 | $null = New-Item -Path $tmpFolder -ItemType Directory 10 | $uri = 'https://github.com/andreasjordan/demos/archive/refs/heads/master.zip' 11 | Invoke-WebRequest -Uri $uri -OutFile "$tmpFolder\master.zip" -UseBasicParsing 12 | Expand-Archive -Path "$tmpFolder\master.zip" -DestinationPath $tmpFolder 13 | Move-Item -Path "$tmpFolder\demos-master\AutomatedLab\CustomAssets" -Destination $labSources 14 | Move-Item -Path "$tmpFolder\demos-master\AutomatedLab\CustomScripts" -Destination $labSources 15 | Remove-Item -Path $tmpFolder -Recurse -Force 16 | -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/DockerImages/README.md: -------------------------------------------------------------------------------- 1 | Exported Docker images are located here so that a download of this data is not necessary as part of `docker pull` or `docker run`. 2 | 3 | The donwload of the Oracle image in particular takes a very long time, so a local copy speeds up the setup of the lab considerably. 4 | 5 | TODO: Share the code to export the images here... 6 | 7 | -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/FileServerISOs/README.md: -------------------------------------------------------------------------------- 1 | ISO files are located here, the contents of which can be copied to a lab's file server. 2 | 3 | File | Size | Description 4 | :------------------------------------------------ | -------: | :------------------------- 5 | en_sql_server_2017_developer_x64_dvd_11296168.iso | 1.475 MB | Microsoft SQL Server 2017 6 | en_sql_server_2019_developer_x64_dvd_e5ade34a.iso | 1.361 MB | Microsoft SQL Server 2019 7 | -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_COMP_CredSSP_Server/manifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_COMP_CredSSP_Server/{A30D2B62-F106-40AD-9C6F-6E410751F850}/Backup.xml: -------------------------------------------------------------------------------- 1 | 2 | 01 00 04 9c 00 00 00 00 00 00 00 00 00 00 00 00 14 00 00 00 04 00 ec 00 08 00 00 00 05 02 28 00 00 01 00 00 01 00 00 00 8f fd ac ed b3 ff d1 11 b4 1d 00 a0 c9 68 f9 39 01 01 00 00 00 00 00 05 0b 00 00 00 00 00 24 00 ff 00 0f 00 01 05 00 00 00 00 00 05 15 00 00 00 0a 7c 75 fe 85 da 01 40 31 ed e5 79 00 02 00 00 00 02 24 00 ff 00 0f 00 01 05 00 00 00 00 00 05 15 00 00 00 0a 7c 75 fe 85 da 01 40 31 ed e5 79 00 02 00 00 00 02 24 00 ff 00 0f 00 01 05 00 00 00 00 00 05 15 00 00 00 0a 7c 75 fe 85 da 01 40 31 ed e5 79 07 02 00 00 00 02 14 00 94 00 02 00 01 01 00 00 00 00 00 05 09 00 00 00 00 02 14 00 94 00 02 00 01 01 00 00 00 00 00 05 0b 00 00 00 00 02 14 00 ff 00 0f 00 01 01 00 00 00 00 00 05 12 00 00 00 00 0a 14 00 ff 00 0f 00 01 01 00 00 00 00 00 03 00 00 00 00 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_COMP_CredSSP_Server/{A30D2B62-F106-40AD-9C6F-6E410751F850}/DomainSysvol/GPO/Machine/comment.cmtx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_COMP_CredSSP_Server/{A30D2B62-F106-40AD-9C6F-6E410751F850}/DomainSysvol/GPO/Machine/registry.pol: -------------------------------------------------------------------------------- 1 | PReg[Software\Policies\Microsoft\Windows\WinRM\Service;AllowCredSSP;;;] -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_COMP_CredSSP_Server/{A30D2B62-F106-40AD-9C6F-6E410751F850}/bkupInfo.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_COMP_CredSSP_Server/{A30D2B62-F106-40AD-9C6F-6E410751F850}/gpreport.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreasjordan/demos/96a0b6df259ae3ce4c1156d6aa2662668f6fd12a/AutomatedLab/CustomAssets/GPOs/GPO_COMP_CredSSP_Server/{A30D2B62-F106-40AD-9C6F-6E410751F850}/gpreport.xml -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_COMP_PasswordAge_0/manifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_COMP_PasswordAge_0/{9900D0B7-C5F0-414E-80D4-9BF07C4CC8C0}/Backup.xml: -------------------------------------------------------------------------------- 1 | 2 | 01 00 04 9c 00 00 00 00 00 00 00 00 00 00 00 00 14 00 00 00 04 00 ec 00 08 00 00 00 05 02 28 00 00 01 00 00 01 00 00 00 8f fd ac ed b3 ff d1 11 b4 1d 00 a0 c9 68 f9 39 01 01 00 00 00 00 00 05 0b 00 00 00 00 00 24 00 ff 00 0f 00 01 05 00 00 00 00 00 05 15 00 00 00 0a 7c 75 fe 85 da 01 40 31 ed e5 79 00 02 00 00 00 02 24 00 ff 00 0f 00 01 05 00 00 00 00 00 05 15 00 00 00 0a 7c 75 fe 85 da 01 40 31 ed e5 79 00 02 00 00 00 02 24 00 ff 00 0f 00 01 05 00 00 00 00 00 05 15 00 00 00 0a 7c 75 fe 85 da 01 40 31 ed e5 79 07 02 00 00 00 02 14 00 94 00 02 00 01 01 00 00 00 00 00 05 09 00 00 00 00 02 14 00 94 00 02 00 01 01 00 00 00 00 00 05 0b 00 00 00 00 02 14 00 ff 00 0f 00 01 01 00 00 00 00 00 05 12 00 00 00 00 0a 14 00 ff 00 0f 00 01 01 00 00 00 00 00 03 00 00 00 00 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_COMP_PasswordAge_0/{9900D0B7-C5F0-414E-80D4-9BF07C4CC8C0}/DomainSysvol/GPO/Machine/microsoft/windows nt/SecEdit/GptTmpl.inf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreasjordan/demos/96a0b6df259ae3ce4c1156d6aa2662668f6fd12a/AutomatedLab/CustomAssets/GPOs/GPO_COMP_PasswordAge_0/{9900D0B7-C5F0-414E-80D4-9BF07C4CC8C0}/DomainSysvol/GPO/Machine/microsoft/windows nt/SecEdit/GptTmpl.inf -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_COMP_PasswordAge_0/{9900D0B7-C5F0-414E-80D4-9BF07C4CC8C0}/bkupInfo.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_COMP_PasswordAge_0/{9900D0B7-C5F0-414E-80D4-9BF07C4CC8C0}/gpreport.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreasjordan/demos/96a0b6df259ae3ce4c1156d6aa2662668f6fd12a/AutomatedLab/CustomAssets/GPOs/GPO_COMP_PasswordAge_0/{9900D0B7-C5F0-414E-80D4-9BF07C4CC8C0}/gpreport.xml -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_COMP_PowerPlan_HighPerformance/manifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_COMP_PowerPlan_HighPerformance/{B69913CD-5BFA-4CCA-8A5E-258B50F4E5A8}/Backup.xml: -------------------------------------------------------------------------------- 1 | 2 | 01 00 04 9c 00 00 00 00 00 00 00 00 00 00 00 00 14 00 00 00 04 00 ec 00 08 00 00 00 05 02 28 00 00 01 00 00 01 00 00 00 8f fd ac ed b3 ff d1 11 b4 1d 00 a0 c9 68 f9 39 01 01 00 00 00 00 00 05 0b 00 00 00 00 00 24 00 ff 00 0f 00 01 05 00 00 00 00 00 05 15 00 00 00 0a 7c 75 fe 85 da 01 40 31 ed e5 79 00 02 00 00 00 02 24 00 ff 00 0f 00 01 05 00 00 00 00 00 05 15 00 00 00 0a 7c 75 fe 85 da 01 40 31 ed e5 79 00 02 00 00 00 02 24 00 ff 00 0f 00 01 05 00 00 00 00 00 05 15 00 00 00 0a 7c 75 fe 85 da 01 40 31 ed e5 79 07 02 00 00 00 02 14 00 94 00 02 00 01 01 00 00 00 00 00 05 09 00 00 00 00 02 14 00 94 00 02 00 01 01 00 00 00 00 00 05 0b 00 00 00 00 02 14 00 ff 00 0f 00 01 01 00 00 00 00 00 05 12 00 00 00 00 0a 14 00 ff 00 0f 00 01 01 00 00 00 00 00 03 00 00 00 00 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_COMP_PowerPlan_HighPerformance/{B69913CD-5BFA-4CCA-8A5E-258B50F4E5A8}/DomainSysvol/GPO/Machine/comment.cmtx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_COMP_PowerPlan_HighPerformance/{B69913CD-5BFA-4CCA-8A5E-258B50F4E5A8}/DomainSysvol/GPO/Machine/registry.pol: -------------------------------------------------------------------------------- 1 | PReg[Software\Policies\Microsoft\Power\PowerSettings;ActivePowerScheme;;J;8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c] -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_COMP_PowerPlan_HighPerformance/{B69913CD-5BFA-4CCA-8A5E-258B50F4E5A8}/bkupInfo.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_COMP_PowerPlan_HighPerformance/{B69913CD-5BFA-4CCA-8A5E-258B50F4E5A8}/gpreport.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreasjordan/demos/96a0b6df259ae3ce4c1156d6aa2662668f6fd12a/AutomatedLab/CustomAssets/GPOs/GPO_COMP_PowerPlan_HighPerformance/{B69913CD-5BFA-4CCA-8A5E-258B50F4E5A8}/gpreport.xml -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_USR_DriveMapping_F_FileServer/manifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_USR_DriveMapping_F_FileServer/{5CB66590-4C6F-4196-AC68-C9F028B5A7CB}/Backup.xml: -------------------------------------------------------------------------------- 1 | 2 | 01 00 04 9c 00 00 00 00 00 00 00 00 00 00 00 00 14 00 00 00 04 00 ec 00 08 00 00 00 05 02 28 00 00 01 00 00 01 00 00 00 8f fd ac ed b3 ff d1 11 b4 1d 00 a0 c9 68 f9 39 01 01 00 00 00 00 00 05 0b 00 00 00 00 00 24 00 ff 00 0f 00 01 05 00 00 00 00 00 05 15 00 00 00 0a 7c 75 fe 85 da 01 40 31 ed e5 79 00 02 00 00 00 02 24 00 ff 00 0f 00 01 05 00 00 00 00 00 05 15 00 00 00 0a 7c 75 fe 85 da 01 40 31 ed e5 79 00 02 00 00 00 02 24 00 ff 00 0f 00 01 05 00 00 00 00 00 05 15 00 00 00 0a 7c 75 fe 85 da 01 40 31 ed e5 79 07 02 00 00 00 02 14 00 94 00 02 00 01 01 00 00 00 00 00 05 09 00 00 00 00 02 14 00 94 00 02 00 01 01 00 00 00 00 00 05 0b 00 00 00 00 02 14 00 ff 00 0f 00 01 01 00 00 00 00 00 05 12 00 00 00 00 0a 14 00 ff 00 0f 00 01 01 00 00 00 00 00 03 00 00 00 00 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_USR_DriveMapping_F_FileServer/{5CB66590-4C6F-4196-AC68-C9F028B5A7CB}/DomainSysvol/GPO/User/Preferences/Drives/Drives.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_USR_DriveMapping_F_FileServer/{5CB66590-4C6F-4196-AC68-C9F028B5A7CB}/bkupInfo.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_USR_DriveMapping_F_FileServer/{5CB66590-4C6F-4196-AC68-C9F028B5A7CB}/gpreport.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreasjordan/demos/96a0b6df259ae3ce4c1156d6aa2662668f6fd12a/AutomatedLab/CustomAssets/GPOs/GPO_USR_DriveMapping_F_FileServer/{5CB66590-4C6F-4196-AC68-C9F028B5A7CB}/gpreport.xml -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_USR_Explorer/manifest.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_USR_Explorer/{DC1CA3A6-3D05-42C2-9A55-EE7C8ECE1919}/Backup.xml: -------------------------------------------------------------------------------- 1 | 2 | 01 00 04 9c 00 00 00 00 00 00 00 00 00 00 00 00 14 00 00 00 04 00 ec 00 08 00 00 00 05 02 28 00 00 01 00 00 01 00 00 00 8f fd ac ed b3 ff d1 11 b4 1d 00 a0 c9 68 f9 39 01 01 00 00 00 00 00 05 0b 00 00 00 00 00 24 00 ff 00 0f 00 01 05 00 00 00 00 00 05 15 00 00 00 0a 7c 75 fe 85 da 01 40 31 ed e5 79 00 02 00 00 00 02 24 00 ff 00 0f 00 01 05 00 00 00 00 00 05 15 00 00 00 0a 7c 75 fe 85 da 01 40 31 ed e5 79 00 02 00 00 00 02 24 00 ff 00 0f 00 01 05 00 00 00 00 00 05 15 00 00 00 0a 7c 75 fe 85 da 01 40 31 ed e5 79 07 02 00 00 00 02 14 00 94 00 02 00 01 01 00 00 00 00 00 05 09 00 00 00 00 02 14 00 94 00 02 00 01 01 00 00 00 00 00 05 0b 00 00 00 00 02 14 00 ff 00 0f 00 01 01 00 00 00 00 00 05 12 00 00 00 00 0a 14 00 ff 00 0f 00 01 01 00 00 00 00 00 03 00 00 00 00 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_USR_Explorer/{DC1CA3A6-3D05-42C2-9A55-EE7C8ECE1919}/DomainSysvol/GPO/User/Preferences/Registry/Registry.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_USR_Explorer/{DC1CA3A6-3D05-42C2-9A55-EE7C8ECE1919}/bkupInfo.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/GPOs/GPO_USR_Explorer/{DC1CA3A6-3D05-42C2-9A55-EE7C8ECE1919}/gpreport.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreasjordan/demos/96a0b6df259ae3ce4c1156d6aa2662668f6fd12a/AutomatedLab/CustomAssets/GPOs/GPO_USR_Explorer/{DC1CA3A6-3D05-42C2-9A55-EE7C8ECE1919}/gpreport.xml -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/SampleDatabases/README.md: -------------------------------------------------------------------------------- 1 | Here are sample databases that can be copied to a lab's file server. 2 | 3 | 4 | File | Size | Description 5 | :----------------------------------- | -------: | :-------------------------------------------------------- 6 | AdventureWorks2012.bak | 45 MB | AdventureWorks for SQL Server 2012 7 | AdventureWorks2014.bak | 45 MB | AdventureWorks for SQL Server 2014 8 | AdventureWorks2016.bak | 46 MB | AdventureWorks for SQL Server 2016 9 | AdventureWorks2017.bak | 48 MB | AdventureWorks for SQL Server 2017 10 | StackOverflowORDIXNewData.bak | 410 MB | StackOverflow in a special ORDIX version (additional data) 11 | StackOverflowORDIXWithoutNewData.bak | 1.362 MB | StackOverflow in a special ORDIX version (Basic version) 12 | -------------------------------------------------------------------------------- /AutomatedLab/CustomAssets/Software/README.md: -------------------------------------------------------------------------------- 1 | Here are software packages that can be copied to a lab's file server. 2 | 3 | 4 | File | Size | Description 5 | :----------------------------------- | -------: | :------------------------------------------------ 6 | OracleXE213_Win64.zip | 1.876 MB | Oracle Database 21.3 Express Edition for Windows 7 | sqldeveloper-22.2.1.234.1810-x64.zip | 444 MB | Oracle SQL Developer 22.2 for Windows 8 | SQLEXPR_x64_ENU.exe | 256 MB | Microsoft SQL Server 2019 Express Edition 9 | WINDOWS.X64_193000_client.zip | 995 MB | Oracle Client 19.3 for Windows 10 | -------------------------------------------------------------------------------- /AutomatedLab/CustomScripts/Docker_Databases.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Continue' 2 | 3 | Import-Module -Name AutomatedLab 4 | Import-Module -Name Posh-SSH 5 | 6 | $LabName = 'DockerDatabases' 7 | $LabNetworkBase = '192.168.123' 8 | 9 | $LabAdminUser = 'User' 10 | $LabAdminPassword = 'Passw0rd!' 11 | 12 | $MachineDefinition = @{ 13 | Name = $LabName 14 | OperatingSystem = 'CentOS-7' 15 | Processors = 4 16 | Network = $LabName 17 | IpAddress = "$LabNetworkBase.10" 18 | Gateway = "$LabNetworkBase.1" 19 | DnsServer1 = '1.1.1.1' 20 | TimeZone = 'W. Europe Standard Time' 21 | } 22 | 23 | <# Some commands that I use for importing, removing, stopping, starting or connecting to the lab: 24 | 25 | Import-Lab -Name $LabName -NoValidation 26 | 27 | Remove-Lab -Name $LabName -Confirm:$false; Get-NetNat -Name $LabName -ErrorAction SilentlyContinue | Remove-NetNat -Confirm:$false 28 | 29 | Stop-VM -Name $MachineDefinition.Name 30 | Start-VM -Name $MachineDefinition.Name 31 | 32 | #> 33 | 34 | $password = $LabAdminPassword 35 | $hostname = 'localhost' 36 | 37 | $DatabaseDefinition = @( 38 | [PSCustomObject]@{ 39 | ContainerName = 'SQLServer' 40 | ContainerImage = 'mcr.microsoft.com/mssql/server:2022-latest' 41 | ContainerMemoryGB = 2 42 | ContainerConfig = "-p 1433:1433 -e MSSQL_SA_PASSWORD='$password' -e ACCEPT_EULA=Y -e MSSQL_PID=Express" 43 | Instance = $hostname 44 | AdminPassword = $password 45 | SqlQueries = @( 46 | "CREATE LOGIN StackOverflow WITH PASSWORD = '$password', CHECK_POLICY = OFF" 47 | 'CREATE DATABASE StackOverflow' 48 | 'ALTER AUTHORIZATION ON DATABASE::StackOverflow TO StackOverflow' 49 | ) 50 | } 51 | [PSCustomObject]@{ 52 | ContainerName = 'Oracle' 53 | ContainerImage = 'container-registry.oracle.com/database/express:latest' 54 | ContainerMemoryGB = 3 55 | ContainerConfig = "-p 1521:1521 -e ORACLE_PWD='$password' -e ORACLE_CHARACTERSET=AL32UTF8" 56 | Instance = "$hostname/XEPDB1" 57 | AdminPassword = $password 58 | SqlQueries = @( 59 | 'CREATE USER stackoverflow IDENTIFIED BY "{0}" DEFAULT TABLESPACE users QUOTA UNLIMITED ON users TEMPORARY TABLESPACE temp' -f $password 60 | 'GRANT CREATE SESSION TO stackoverflow' 61 | 'GRANT ALL PRIVILEGES TO stackoverflow' 62 | 'CREATE USER geodemo IDENTIFIED BY "{0}" DEFAULT TABLESPACE users QUOTA UNLIMITED ON users TEMPORARY TABLESPACE temp' -f $password 63 | 'GRANT CREATE SESSION TO geodemo' 64 | 'GRANT ALL PRIVILEGES TO geodemo' 65 | ) 66 | } 67 | [PSCustomObject]@{ 68 | ContainerName = 'MySQL' 69 | ContainerImage = 'mysql:latest' 70 | ContainerMemoryGB = 1 71 | ContainerConfig = "-p 3306:3306 -e MYSQL_ROOT_PASSWORD='$password'" 72 | Instance = $hostname 73 | AdminPassword = $password 74 | SqlQueries = @( 75 | #'SET GLOBAL local_infile=1' 76 | 'SET PERSIST local_infile=1' 77 | "CREATE USER 'stackoverflow'@'%' IDENTIFIED BY '$password'" 78 | 'CREATE DATABASE stackoverflow' 79 | "GRANT ALL PRIVILEGES ON stackoverflow.* TO 'stackoverflow'@'%'" 80 | ) 81 | } 82 | [PSCustomObject]@{ 83 | ContainerName = 'MariaDB' 84 | ContainerImage = 'mariadb:latest' 85 | ContainerMemoryGB = 1 86 | ContainerConfig = "-p 13306:3306 -e MARIADB_ROOT_PASSWORD='$password'" 87 | Instance = "$($hostname):13306" 88 | AdminPassword = $password 89 | SqlQueries = @( 90 | "CREATE USER 'stackoverflow'@'%' IDENTIFIED BY '$password'" 91 | 'CREATE DATABASE stackoverflow' 92 | "GRANT ALL PRIVILEGES ON stackoverflow.* TO 'stackoverflow'@'%'" 93 | ) 94 | } 95 | [PSCustomObject]@{ 96 | ContainerName = 'PostgreSQL' 97 | ContainerImage = 'postgres:latest' 98 | ContainerMemoryGB = 1 99 | ContainerConfig = "-p 5432:5432 -e POSTGRES_PASSWORD='$password'" 100 | Instance = $hostname 101 | AdminPassword = $password 102 | SqlQueries = @( 103 | "CREATE USER stackoverflow WITH PASSWORD '$password'" 104 | 'CREATE DATABASE stackoverflow WITH OWNER stackoverflow' 105 | ) 106 | } 107 | [PSCustomObject]@{ 108 | ContainerName = 'PostGIS' 109 | ContainerImage = 'postgis/postgis' 110 | ContainerMemoryGB = 1 111 | ContainerConfig = "-p 15432:5432 -e POSTGRES_PASSWORD='$password'" 112 | Instance = "$($hostname):15432" 113 | AdminPassword = $password 114 | SqlQueries = @( 115 | "CREATE USER geodemo WITH PASSWORD '$password'" 116 | 'CREATE DATABASE geodemo WITH OWNER geodemo' 117 | '\connect geodemo' 118 | 'CREATE EXTENSION postgis' 119 | ) 120 | } 121 | ) 122 | 123 | # $DatabaseDefinition = $DatabaseDefinition | Where-Object ContainerName -in SQLServer, Oracle 124 | $DatabaseDefinition = $DatabaseDefinition | Out-GridView -Title 'Select docker conatiners to start' -OutputMode Multiple 125 | 126 | 127 | ### End of configuration ### 128 | 129 | 130 | $MachineDefinition.Memory = 2GB + ($DatabaseDefinition.ContainerMemoryGB | Measure-Object -Sum).Sum * 1GB 131 | 132 | Set-PSFConfig -Module AutomatedLab -Name DoNotWaitForLinux -Value $true 133 | 134 | New-LabDefinition -Name $LabName -DefaultVirtualizationEngine HyperV 135 | Set-LabInstallationCredential -Username $LabAdminUser -Password $LabAdminPassword 136 | Add-LabVirtualNetworkDefinition -Name $LabName -AddressSpace "$LabNetworkBase.0/24" 137 | Add-LabMachineDefinition @MachineDefinition 138 | Install-Lab -NoValidation 139 | 140 | $null = New-NetNat -Name $LabName -InternalIPInterfaceAddressPrefix "$LabNetworkBase.0/24" 141 | 142 | Start-LabVM -ComputerName $MachineDefinition.Name 143 | 144 | # Now the linux maschine is started and will be installed automatically. We just have to wait... 145 | 146 | 147 | Write-PSFMessage -Level Host -Message "Waiting until we can connect and open ssh and sftp session" 148 | $rootCredential = [PSCredential]::new('root', (ConvertTo-SecureString -String $LabAdminPassword -AsPlainText -Force)) 149 | $linuxIp = $MachineDefinition.IpAddress 150 | while ($true) { 151 | try { 152 | if (-not $sshSession) { 153 | $sshSession = New-SSHSession -ComputerName $linuxIp -Credential $rootCredential -Force -WarningAction SilentlyContinue -ErrorAction Stop 154 | } 155 | if (-not $sftpSession) { 156 | $sftpSession = New-SFTPSession -ComputerName $linuxIp -Credential $rootCredential -Force -WarningAction SilentlyContinue -ErrorAction Stop 157 | } 158 | break 159 | } catch { 160 | Write-PSFMessage -Level Host -Message "Still waiting ... [$($sshSession.Connected) / $($sftpSession.Connected)] ($_)" 161 | Start-Sleep -Seconds 60 162 | } 163 | } 164 | 165 | 166 | # As we do a lot of ssh commands via Invoke-SSHCommand from Posh-SSH lets use a wrapper to save some lines of code 167 | function Invoke-MySSHCommand { 168 | param( 169 | [SSH.SshSession]$SSHSession, 170 | [string[]]$Command, 171 | [int]$TimeOut = 9999, 172 | [switch]$ShowOutput 173 | ) 174 | $returnValue = $true 175 | foreach ($cmd in $Command) { 176 | while ($true) { 177 | try { 178 | $sshResult = Invoke-SSHCommand -SSHSession $SSHSession -Command $cmd -EnsureConnection -TimeOut $TimeOut -ShowStandardOutputStream:$ShowOutput -ShowErrorOutputStream:$ShowOutput -ErrorAction Stop 179 | break 180 | } catch { 181 | Write-PSFMessage -Level Host -Message "Command '$cmd' failed with $_" 182 | Start-Sleep -Seconds 10 183 | } 184 | } 185 | if ($sshResult.ExitStatus -gt 0) { 186 | Write-PSFMessage -Level Warning -Message "Command '$cmd' returned with ExitStatus $($sshResult.ExitStatus)" 187 | $returnValue = $false 188 | break 189 | } 190 | } 191 | return $returnValue 192 | } 193 | 194 | # To stop the execution if an ssh command failed we use this command in a pipeline after Invoke-MySSHCommand 195 | function Stop-ExecutionOnFailure { 196 | param( 197 | [Parameter(ValueFromPipeline = $true)][boolean]$InputObject 198 | ) 199 | if (-not $InputObject) { 200 | break 201 | } 202 | } 203 | 204 | 205 | Write-PSFMessage -Level Host -Message "Updating packages on Linux" 206 | $sshCommands = @( 207 | 'yum -y update' 208 | ) 209 | Invoke-MySSHCommand -SSHSession $sshSession -Command $sshCommands | Stop-ExecutionOnFailure 210 | 211 | Write-PSFMessage -Level Host -Message "Installing docker on Linux" 212 | $sshCommands = @( 213 | 'yum install -y yum-utils' 214 | 'yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo' 215 | 'yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin' 216 | 'systemctl enable --now docker' 217 | ) 218 | Invoke-MySSHCommand -SSHSession $sshSession -Command $sshCommands | Stop-ExecutionOnFailure 219 | 220 | Write-PSFMessage -Level Host -Message "Installing 7zip on Linux" 221 | $sshCommands = @( 222 | 'yum install -y epel-release' 223 | 'yum install -y p7zip' 224 | ) 225 | Invoke-MySSHCommand -SSHSession $sshSession -Command $sshCommands | Stop-ExecutionOnFailure 226 | 227 | Write-PSFMessage -Level Host -Message "Installing PowerShell modules" 228 | $sshCommands = @( 229 | "pwsh -c 'Set-PSRepository -Name PSGallery -InstallationPolicy Trusted'" 230 | "pwsh -c 'Install-Module -Name PSFramework'" 231 | "pwsh -c 'Install-Module -Name Microsoft.PowerShell.ConsoleGuiTools'" 232 | ) 233 | Invoke-MySSHCommand -SSHSession $sshSession -Command $sshCommands | Stop-ExecutionOnFailure 234 | 235 | Write-PSFMessage -Level Host -Message "Copying and loading or pulling and saving docker images" 236 | foreach ($db in $DatabaseDefinition) { 237 | $imagePath = "$labSources\CustomAssets\DockerImages\$($db.ContainerName).tar.gz" 238 | if (Test-Path -Path $imagePath) { 239 | Write-PSFMessage -Level Host -Message "Found saved image for $($db.ContainerName), will copy and load image" 240 | Set-SFTPItem -SFTPSession $sftpSession -Destination /tmp -Path $imagePath -ErrorAction Stop 241 | $sshCommands = @( 242 | "docker load -i /tmp/$($db.ContainerName).tar.gz" 243 | "rm /tmp/$($db.ContainerName).tar.gz" 244 | ) 245 | Invoke-MySSHCommand -SSHSession $sshSession -Command $sshCommands | Stop-ExecutionOnFailure 246 | } else { 247 | Write-PSFMessage -Level Host -Message "No saved image found for $($db.ContainerName), will pull and save image" 248 | $sshCommands = @( 249 | "docker pull $($db.ContainerImage)" 250 | "docker save -o /tmp/$($db.ContainerName).tar $($db.ContainerImage)" 251 | "gzip /tmp/$($db.ContainerName).tar" 252 | ) 253 | Invoke-MySSHCommand -SSHSession $sshSession -Command $sshCommands | Stop-ExecutionOnFailure 254 | Get-SFTPItem -SFTPSession $sftpSession -Path "/tmp/$($db.ContainerName).tar.gz" -Destination "$labSources\CustomAssets\DockerImages" -ErrorAction Stop 255 | } 256 | } 257 | 258 | Write-PSFMessage -Level Host -Message "Starting databases on docker" 259 | foreach ($db in $DatabaseDefinition) { 260 | Write-PSFMessage -Level Host -Message "Starting docker container $($db.ContainerName)" 261 | $sshCommands = @( 262 | "docker run --name $($db.ContainerName) --memory=$($db.ContainerMemoryGB)g $($db.ContainerConfig) --detach --restart always $($db.ContainerImage)" 263 | ) 264 | Invoke-MySSHCommand -SSHSession $sshSession -Command $sshCommands | Stop-ExecutionOnFailure 265 | } 266 | 267 | Write-PSFMessage -Level Host -Message "Downloading PowerShell-for-DBAs" 268 | $sshCommands = @( 269 | 'curl -sL https://github.com/andreasjordan/PowerShell-for-DBAs/tarball/main | tar zx --transform "s,^[^/]+,PowerShell-for-DBAs,x"' 270 | ) 271 | Invoke-MySSHCommand -SSHSession $sshSession -Command $sshCommands | Stop-ExecutionOnFailure 272 | 273 | Write-PSFMessage -Level Host -Message "Copying database definition information to Linux" 274 | $DatabaseDefinition | ConvertTo-Json | Set-Content -Path "$labSources\tmp_DatabaseDefinition.json" 275 | Set-SFTPItem -SFTPSession $sftpSession -Destination /tmp -Path "$labSources\tmp_DatabaseDefinition.json" -ErrorAction Stop 276 | Remove-Item -Path "$labSources\tmp_DatabaseDefinition.json" 277 | 278 | Write-PSFMessage -Level Host -Message "Creating sample databases" 279 | $sshCommands = @( 280 | 'pwsh ./PowerShell-for-DBAs/PowerShell/01_SetupSampleDatabases.ps1' 281 | ) 282 | Invoke-MySSHCommand -SSHSession $sshSession -Command $sshCommands -ShowOutput | Stop-ExecutionOnFailure 283 | 284 | Write-PSFMessage -Level Host -Message "Creating sample schemas" 285 | $sshCommands = @( 286 | 'pwsh ./PowerShell-for-DBAs/PowerShell/02_SetupSampleSchemas.ps1' 287 | ) 288 | Invoke-MySSHCommand -SSHSession $sshSession -Command $sshCommands -ShowOutput | Stop-ExecutionOnFailure 289 | 290 | Write-PSFMessage -Level Host -Message "Importing sample data from JSON" 291 | $sshCommands = @( 292 | 'pwsh ./PowerShell-for-DBAs/PowerShell/03_ImportSampleDataFromJson.ps1' 293 | ) 294 | Invoke-MySSHCommand -SSHSession $sshSession -Command $sshCommands -ShowOutput | Stop-ExecutionOnFailure 295 | 296 | Write-PSFMessage -Level Host -Message "Importing sample data from Stackexchange" 297 | $sshCommands = @( 298 | 'pwsh ./PowerShell-for-DBAs/PowerShell/04_ImportSampleDataFromStackexchange.ps1' 299 | ) 300 | Invoke-MySSHCommand -SSHSession $sshSession -Command $sshCommands -ShowOutput | Stop-ExecutionOnFailure 301 | 302 | Write-PSFMessage -Level Host -Message "Importing sample geographic data" 303 | $sshCommands = @( 304 | 'pwsh ./PowerShell-for-DBAs/PowerShell/05_ImportSampleGeographicData.ps1' 305 | ) 306 | Invoke-MySSHCommand -SSHSession $sshSession -Command $sshCommands -ShowOutput | Stop-ExecutionOnFailure 307 | 308 | Write-PSFMessage -Level Host -Message "finished" 309 | 310 | <# 311 | 312 | Logging: 313 | 314 | 11:11:23|00:00:00|00:00:00.000| Initialization 315 | 11:11:23|00:00:00|00:00:00.167| - Host operating system version: 'Microsoft Windows 10 Pro, 10.0.19045.0' 316 | 11:11:23|00:00:00|00:00:00.175| - Creating new lab definition with name 'DockerDatabases' 317 | 11:11:23|00:00:00|00:00:00.190| - Location of lab definition files will be 'C:\ProgramData\AutomatedLab/Labs/DockerDatabases' 318 | 11:11:24|00:00:00|00:00:00.817| - Location of LabSources folder is 'C:\AutomatedLab-Sources' 319 | 11:11:24|00:00:00|00:00:00.000| - Auto-adding ISO files 320 | 11:11:24|00:00:01|00:00:00.508| - Added 'C:\AutomatedLab-Sources\ISOs\2022_x64_EN_Eval.iso' 321 | 11:11:24|00:00:01|00:00:00.523| - Added 'C:\AutomatedLab-Sources\ISOs\CentOS-7-x86_64-DVD-2207-02.iso' 322 | 11:11:25|00:00:01|00:00:00.532| - Done 323 | 11:11:25|00:00:01|00:00:00.555| - Path for VMs specified as 'C:\AutomatedLab-VMs' 324 | 11:11:25|00:00:01|00:00:00.000| - Adding Hyper-V machine definition 'DockerDatabases' 325 | 11:11:27|00:00:04|00:00:02.080| - Done 326 | 11:11:28|00:00:04|00:00:02.556| Estimated (additional) local drive space needed for all machines: 2 GB 327 | 11:11:28|00:00:04|00:00:02.600| Location of Hyper-V machines will be 'C:\AutomatedLab-VMs' 328 | 11:11:28|00:00:04|00:00:02.901| Done 329 | 11:11:29|00:00:05|00:00:03.644| Lab 'DockerDatabases' hosted on 'HyperV' imported with 1 machines 330 | 11:11:29|00:00:06|00:00:00.000| Creating virtual networks 331 | 11:11:29|00:00:06|00:00:00.000| - Creating Hyper-V virtual network 'DockerDatabases' 332 | 11:11:33|00:00:10|00:00:04.061| - Done 333 | 11:11:34|00:00:10|00:00:04.157| - Done 334 | 11:11:34|00:00:10|00:00:04.232| done 335 | 11:11:34|00:00:10|00:00:00.000| Creating Additional Disks 336 | 11:11:34|00:00:10|00:00:00.142| - Done 337 | 11:11:34|00:00:10|00:00:00.000| Creating VMs 338 | 11:11:34|00:00:10|00:00:00.187| - The hosts file has been added 1 records. Clean them up using 'Remove-Lab' or manually if needed 339 | 11:11:34|00:00:10|00:00:00.000| - Waiting for all machines to finish installing 340 | 11:11:34|00:00:10|00:00:00.000| - Creating HyperV machine 'DockerDatabases'.... 341 | 11:11:45|00:00:21|00:00:10.469| - Done 342 | 11:11:45|00:00:21|00:00:10.475| - Done 343 | 11:11:45|00:00:21|00:00:10.777| - Done 344 | 11:11:45|00:00:22|00:00:00.000| Starting remaining machines 345 | 11:11:45|00:00:22|00:00:00.022| - Waiting for machines to start up...Done 346 | 11:11:46|00:00:22|00:00:00.000| Installing RDS certificates of lab machines 347 | 11:11:46|00:00:22|00:00:00.034| - Done 348 | 11:11:48|00:00:25|00:00:02.688| . 349 | [11:11:48][Docker_Databases.ps1] Waiting until we can connect and open ssh and sftp session 350 | [11:11:58][Docker_Databases.ps1] Still waiting ... [ / ] (Connection failed to establish within 10000 milliseconds.) 351 | [11:13:09][Docker_Databases.ps1] Still waiting ... [ / ] (Connection failed to establish within 10000 milliseconds.) 352 | [11:14:11][Docker_Databases.ps1] Still waiting ... [ / ] (Es konnte keine Verbindung hergestellt werden, da der Zielcomputer die Verbindung verweigerte) 353 | [11:15:13][Docker_Databases.ps1] Still waiting ... [ / ] (Es konnte keine Verbindung hergestellt werden, da der Zielcomputer die Verbindung verweigerte) 354 | [11:16:15][Docker_Databases.ps1] Still waiting ... [ / ] (Es konnte keine Verbindung hergestellt werden, da der Zielcomputer die Verbindung verweigerte) 355 | [11:17:15][Docker_Databases.ps1] Updating packages on Linux 356 | [11:18:28][Docker_Databases.ps1] Installing docker on Linux 357 | [11:19:00][Docker_Databases.ps1] Installing 7zip on Linux 358 | [11:19:10][Docker_Databases.ps1] Installing PowerShell modules 359 | [11:19:30][Docker_Databases.ps1] Copying and loading or pulling and saving docker images 360 | [11:19:30][Docker_Databases.ps1] Found saved image for SQLServer, will copy and load image 361 | [11:20:05][Docker_Databases.ps1] Found saved image for Oracle, will copy and load image 362 | [11:23:54][Docker_Databases.ps1] Found saved image for MySQL, will copy and load image 363 | [11:24:06][Docker_Databases.ps1] Found saved image for MariaDB, will copy and load image 364 | [11:24:16][Docker_Databases.ps1] Found saved image for PostgreSQL, will copy and load image 365 | [11:24:26][Docker_Databases.ps1] Found saved image for PostGIS, will copy and load image 366 | [11:24:37][Docker_Databases.ps1] Starting databases on docker 367 | [11:24:37][Docker_Databases.ps1] Starting docker container SQLServer 368 | [11:24:38][Docker_Databases.ps1] Starting docker container Oracle 369 | [11:24:39][Docker_Databases.ps1] Starting docker container MySQL 370 | [11:24:39][Docker_Databases.ps1] Starting docker container MariaDB 371 | [11:24:39][Docker_Databases.ps1] Starting docker container PostgreSQL 372 | [11:24:40][Docker_Databases.ps1] Starting docker container PostGIS 373 | [11:24:41][Docker_Databases.ps1] Downloading PowerShell-for-DBAs 374 | [11:24:42][Docker_Databases.ps1] Copying database definition information to Linux 375 | [11:24:42][Docker_Databases.ps1] Creating sample databases 376 | [09:24:47][01_SetupSampleDatabases.ps1] Waiting for connection to SQL Server 377 | [09:25:18][01_SetupSampleDatabases.ps1] Creating sample database and user on SQL Server finished 378 | [09:25:20][01_SetupSampleDatabases.ps1] Waiting for connection to Oracle 379 | [09:25:20][01_SetupSampleDatabases.ps1] Creating sample users on Oracle finished 380 | [09:25:27][01_SetupSampleDatabases.ps1] Waiting for connection to MySQL 381 | [09:25:27][01_SetupSampleDatabases.ps1] Creating sample database and user on MySQL finished 382 | [09:25:27][01_SetupSampleDatabases.ps1] Waiting for connection to MariaDB 383 | [09:25:27][01_SetupSampleDatabases.ps1] Creating sample database and user on MariaDB finished 384 | [09:25:31][01_SetupSampleDatabases.ps1] Waiting for connection to PostgreSQL 385 | [09:25:31][01_SetupSampleDatabases.ps1] Creating sample database and user on PostgreSQL finished 386 | [09:25:31][01_SetupSampleDatabases.ps1] Waiting for connection to PostGIS 387 | [09:25:31][01_SetupSampleDatabases.ps1] Creating sample database and user on PostGIS finished 388 | [11:25:31][Docker_Databases.ps1] Creating sample schemas 389 | [09:25:33][02_SetupSampleSchemas.ps1] Creating sample schema on SQL Server finished 390 | [09:25:34][02_SetupSampleSchemas.ps1] Creating sample schema on Oracle finished 391 | [09:25:34][02_SetupSampleSchemas.ps1] Creating sample schema on MySQL finished 392 | [09:25:35][02_SetupSampleSchemas.ps1] Creating sample schema on MariaDB finished 393 | [09:25:35][02_SetupSampleSchemas.ps1] Creating sample schema on PostgreSQL finished 394 | [09:25:35][02_SetupSampleSchemas.ps1] Creating sample schema on PostGIS finished 395 | [11:25:34][Docker_Databases.ps1] Importing sample data from JSON 396 | [09:25:40][03_ImportSampleDataFromJson.ps1] Importing sample data to SQL Server finished in 3.0137833 seconds 397 | [09:25:46][03_ImportSampleDataFromJson.ps1] Importing sample data to Oracle finished in 5.2271296 seconds 398 | [09:25:48][03_ImportSampleDataFromJson.ps1] Importing sample data to MySQL finished in 2.2646515 seconds 399 | [09:25:50][03_ImportSampleDataFromJson.ps1] Importing sample data to MariaDB finished in 1.801707 seconds 400 | [09:26:14][03_ImportSampleDataFromJson.ps1] Importing sample data to PostgreSQL finished in 23.9747867 seconds 401 | [11:26:13][Docker_Databases.ps1] Importing sample data from Stackexchange 402 | [09:26:20][04_ImportSampleDataFromStackexchange.ps1] Dowload sample data finished in 4.0068473 seconds 403 | [09:26:25][04_ImportSampleDataFromStackexchange.ps1] Importing sample data to SQL Server finished in 5.9283759 seconds 404 | [09:26:34][04_ImportSampleDataFromStackexchange.ps1] Importing sample data to Oracle finished in 8.6976567 seconds 405 | [09:26:41][04_ImportSampleDataFromStackexchange.ps1] Importing sample data to MySQL finished in 6.5348029 seconds 406 | [09:26:46][04_ImportSampleDataFromStackexchange.ps1] Importing sample data to MariaDB finished in 5.244922 seconds 407 | [09:27:40][04_ImportSampleDataFromStackexchange.ps1] Importing sample data to PostgreSQL finished in 54.2771984 seconds 408 | [11:27:40][Docker_Databases.ps1] Importing sample geographic data 409 | [09:27:52][05_ImportSampleGeographicData.ps1] Importing sample geographic data to Oracle finished in 4.8504032 seconds 410 | [09:27:54][05_ImportSampleGeographicData.ps1] Importing sample geographic data to PostGIS finished in 2.638402 seconds 411 | [11:27:54][Docker_Databases.ps1] finished 412 | 413 | #> -------------------------------------------------------------------------------- /AutomatedLab/CustomScripts/README.md: -------------------------------------------------------------------------------- 1 | Here are the scripts for building different labs. 2 | 3 | All scripts must be executed in an administrative PowerShell. 4 | 5 | Most scripts need additional components from the CustomAssets folder. 6 | 7 | 8 | 9 | ## Docker_Databases 10 | 11 | This lab consists of a Linux VM with CentOS-7 and docker, as well as containers with various database systems. 12 | 13 | Currently possible: 14 | * Microsoft SQL Server 15 | * Oracle 16 | * MySQL 17 | * MariaDB 18 | * PostgreSQL 19 | * PostgreSQL with PostGIS 20 | 21 | A test user is also created in each of the database systems and test data is loaded. Code from my GitHub repository https://github.com/andreasjordan/PowerShell-for-DBAs is used for this. 22 | 23 | Currently possible: 24 | * StackOverflow's database with the same schema and sample data for all database systems. All information comes from the mentioned GitHub repository. 25 | * Polygons of country boundaries. The data is downloaded directly from https://datahub.io/core/geo-countries. 26 | 27 | The script is currently "in progress". Additional wishes and comments are welcome. 28 | 29 | 30 | ## SQLServer_AlwaysOn 31 | 32 | This lab is specifically intended for running the SQL Server Always On Availability Groups demos, which are stored here: https://github.com/andreasjordan/demos/tree/master/AlwaysOn 33 | 34 | Therefore, the configuration of the lab may only be adjusted if the demo scripts are adjusted at the same time. 35 | 36 | The lab consists of five Windows Server 2022 machines, one of which is a domain controller, one server on which the demo scripts are executed and three servers with SQL Server instances. 37 | 38 | 39 | ## Win2022_Oracle 40 | 41 | The lab consists of three Windows Server 2022 machines. The Oracle client is installed on one server, and an Oracle server on each of the other two. 42 | 43 | 44 | ## Win2022_SQLServer 45 | 46 | The lab consists of a Windows Server 2022 machine with an instance of SQL Server 2019 Express installed. 47 | -------------------------------------------------------------------------------- /AutomatedLab/CustomScripts/Win2022_Oracle.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Continue' 2 | 3 | Import-Module -Name AutomatedLab 4 | 5 | $LabName = "WinOra" 6 | 7 | 8 | <# Some commands that I use for importing, removing, stopping, starting or connecting to the lab: 9 | 10 | Import-Lab -Name $LabName -NoValidation 11 | 12 | Remove-Lab -Name $LabName -Confirm:$false; Get-NetNat -Name $LabName -ErrorAction SilentlyContinue | Remove-NetNat -Confirm:$false 13 | 14 | Stop-VM -Name $LabName-* 15 | Start-VM -Name $LabName-* 16 | 17 | vmconnect.exe localhost $LabName-WIN-CL 18 | 19 | #> 20 | 21 | 22 | $LabNetworkBase = '192.168.111' 23 | $LabDnsServer = '1.1.1.1' 24 | 25 | $LabAdminUser = 'User' 26 | $LabAdminPassword = 'Passw0rd!' 27 | 28 | $MachineDefinition = @( 29 | @{ 30 | Name = 'WIN-CL' 31 | ResourceName = "$LabName-WIN-CL" 32 | OperatingSystem = 'Windows Server 2022 Standard Evaluation (Desktop Experience)' 33 | Memory = 4GB 34 | Processors = 4 35 | Network = $LabName 36 | IpAddress = "$LabNetworkBase.10" 37 | Gateway = "$LabNetworkBase.1" 38 | DnsServer1 = $LabDnsServer 39 | TimeZone = 'W. Europe Standard Time' 40 | } 41 | @{ 42 | Name = 'WIN-DB01' 43 | ResourceName = "$LabName-WIN-DB01" 44 | OperatingSystem = 'Windows Server 2022 Standard Evaluation (Desktop Experience)' 45 | Memory = 4GB 46 | Processors = 4 47 | Network = $LabName 48 | IpAddress = "$LabNetworkBase.11" 49 | Gateway = "$LabNetworkBase.1" 50 | DnsServer1 = $LabDnsServer 51 | TimeZone = 'W. Europe Standard Time' 52 | } 53 | @{ 54 | Name = 'WIN-DB02' 55 | ResourceName = "$LabName-WIN-DB02" 56 | OperatingSystem = 'Windows Server 2022 Standard Evaluation (Desktop Experience)' 57 | Memory = 4GB 58 | Processors = 4 59 | Network = $LabName 60 | IpAddress = "$LabNetworkBase.12" 61 | Gateway = "$LabNetworkBase.1" 62 | DnsServer1 = $LabDnsServer 63 | TimeZone = 'W. Europe Standard Time' 64 | } 65 | ) 66 | 67 | $ChocolateyPackages = @( 68 | 'notepadplusplus' 69 | '7zip' 70 | ) 71 | 72 | $CopySoftware = @( 73 | "$labSources\CustomAssets\Software\OracleXE213_Win64.zip" # Oracle Express for Windows from: https://www.oracle.com/database/technologies/xe-downloads.html 74 | "$labSources\CustomAssets\Software\WINDOWS.X64_193000_client.zip" # Oracle Client 19c from: https://www.oracle.com/database/technologies/oracle19c-windows-downloads.html 75 | ) 76 | 77 | 78 | 79 | New-LabDefinition -Name $LabName -DefaultVirtualizationEngine HyperV -VmPath $LabVmPath 80 | Set-LabInstallationCredential -Username $LabAdminUser -Password $LabAdminPassword 81 | Add-LabVirtualNetworkDefinition -Name $LabName -AddressSpace "$LabNetworkBase.0/24" 82 | foreach ($md in $MachineDefinition) { 83 | Add-LabMachineDefinition @md 84 | } 85 | Install-Lab -NoValidation 86 | 87 | # I use NetNat to provide internat to the virtual maschines 88 | $null = New-NetNat -Name $LabName -InternalIPInterfaceAddressPrefix "$LabNetworkBase.0/24" 89 | 90 | 91 | $allVMs = @( ) 92 | $clientVMs = @( ) 93 | $databaseVMs = @( ) 94 | $allVMs += Get-LabVM 95 | $clientVMs += $allVMs | Where-Object Name -like "*-CL*" 96 | $databaseVMs += $allVMs | Where-Object Name -like "*-DB*" 97 | 98 | 99 | Invoke-LabCommand -ComputerName $allVMs.Name -ActivityName 'Disable Windows updates' -ScriptBlock { 100 | # https://learn.microsoft.com/en-us/windows/deployment/update/waas-wu-settings 101 | Set-ItemProperty -Path HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU -Name NoAutoUpdate -Value 1 102 | } 103 | 104 | Invoke-LabCommand -ComputerName $allVMs.Name -ActivityName 'Setting my favorite explorer settings' -ScriptBlock { 105 | Set-ItemProperty -Path HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced -Name HideFileExt -Value 0 106 | Set-ItemProperty -Path HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced -Name NavPaneShowAllFolders -Value 1 107 | Set-ItemProperty -Path HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced -Name NavPaneExpandToCurrentFolder -Value 1 108 | } 109 | 110 | if ($HostEntries.Count -gt 0) { 111 | Invoke-LabCommand -ComputerName $allVMs.Name -ActivityName 'SetupHostEntries' -ArgumentList @(, $HostEntries) -ScriptBlock { 112 | param($HostEntries) 113 | 114 | $HostEntries | Add-Content -Path C:\Windows\System32\drivers\etc\hosts 115 | } 116 | } 117 | 118 | 119 | foreach ($file in $CopySoftware) { 120 | Copy-LabFileItem -Path $file -ComputerName $allVMs.Name -DestinationFolderPath C:\Software 121 | } 122 | 123 | 124 | Invoke-LabCommand -ComputerName $databaseVMs.Name -ActivityName 'Installing Oracle Server' -ArgumentList $LabAdminPassword -ScriptBlock { 125 | param($Password) 126 | $rspContent = @( 127 | 'INSTALLDIR=C:\oracle\product\21c\' 128 | "PASSWORD=$Password" 129 | 'LISTENER_PORT=1521' 130 | 'EMEXPRESS_PORT=5550' 131 | 'CHAR_SET=AL32UTF8' 132 | 'DB_DOMAIN=' 133 | ) 134 | $argumentList = @( 135 | '/s' 136 | '/v"RSP_FILE=C:\Software\OracleServerInstall.rsp"' 137 | '/v"/L*v C:\Software\OracleServerSetup.log"' 138 | '/v"/qn"' 139 | ) 140 | Expand-Archive -Path C:\Software\OracleXE213_Win64.zip -DestinationPath C:\Software\OracleXE213_Win64 141 | $rspContent | Set-Content -Path C:\Software\OracleServerInstall.rsp 142 | Start-Process -FilePath C:\Software\OracleXE213_Win64\setup.exe -ArgumentList $argumentList -WorkingDirectory C:\Software -NoNewWindow -Wait 143 | } 144 | 145 | $numberOfOracleServices = Invoke-LabCommand -ComputerName $databaseVMs.Name -ActivityName 'Testing Oracle Server' -PassThru -ScriptBlock { 146 | (Get-Service | Where-Object Name -like Oracle*).Count 147 | } 148 | 149 | if ($numberOfOracleServices -lt 5) { 150 | Write-Warning -Message "We only have $numberOfOracleServices oracle services - so installation failed. Please rebuild the lab, will start removing the lab now..." 151 | Remove-Lab -Name $LabName -Confirm:$false 152 | Get-NetNat -Name $LabName -ErrorAction SilentlyContinue | Remove-NetNat -Confirm:$false 153 | break 154 | } 155 | 156 | 157 | 158 | <# 159 | 160 | #------------------------------------------------------------------------------ 161 | #Name : INSTALL_TYPE 162 | #Datatype : String 163 | #Description: Installation type of the component. 164 | # 165 | # The following choices are available. The value should contain 166 | # only one of these choices. 167 | # - Administrator 168 | # - Runtime 169 | # - InstantClient 170 | # - Custom 171 | # 172 | #Example : INSTALL_TYPE = Administrator 173 | #------------------------------------------------------------------------------ 174 | 175 | #------------------------------------------------------------------------------- 176 | # Name : oracle.install.client.customComponents 177 | # Datatype : StringList 178 | # 179 | # This property is considered only if INSTALL_TYPE is set to "Custom" 180 | # 181 | # Description: List of Client Components you would like to install 182 | # 183 | # The following choices are available. You may specify any 184 | # combination of these choices. The components you choose should 185 | # be specified in the form "internal-component-name:version" 186 | # Below is a list of components you may specify to install. 187 | # 188 | # oracle.sqlj:19.0.0.0.0 -- "Oracle SQLJ" 189 | # oracle.rdbms.util:19.0.0.0.0 -- "Oracle Database Utilities" 190 | # oracle.javavm.client:19.0.0.0.0 -- "Oracle Java Client" 191 | # oracle.sqlplus:19.0.0.0.0 -- "SQL*Plus" 192 | # oracle.dbjava.jdbc:19.0.0.0.0 -- "Oracle JDBC/THIN Interfaces" 193 | # oracle.ldap.client:19.0.0.0.0 -- "Oracle Internet Directory Client" 194 | # oracle.rdbms.oci:19.0.0.0.0 -- "Oracle Call Interface (OCI)" 195 | # oracle.precomp:19.0.0.0.0 -- "Oracle Programmer" 196 | # oracle.xdk:19.0.0.0.0 -- "Oracle XML Development Kit" 197 | # oracle.network.aso:19.0.0.0.0 -- "Oracle Advanced Security" 198 | # oracle.oraolap.mgmt:19.0.0.0.0 -- "OLAP Analytic Workspace Manager and Worksheet" 199 | # oracle.network.client:19.0.0.0.0 -- "Oracle Net" 200 | # oracle.network.cman:19.0.0.0.0 -- "Oracle Connection Manager" 201 | # oracle.network.listener:19.0.0.0.0 -- "Oracle Net Listener" 202 | # oracle.ordim.client:19.0.0.0.0 -- "Oracle Multimedia Client Option" 203 | # oracle.odbc:19.0.0.0.0 -- "Oracle ODBC Driver" 204 | # oracle.has.client:19.0.0.0.0 -- "Oracle Clusterware High Availability API" 205 | # oracle.dbdev:19.0.0.0.0 -- "Oracle SQL Developer" 206 | # oracle.rdbms.scheduler:19.0.0.0.0 -- "Oracle Scheduler Agent" 207 | # oracle.ntoramts:19.0.0.0.0 -- "Oracle Services For Microsoft Transaction Server" 208 | # oracle.ntoledb:19.0.0.0.0 -- "Oracle Provider for OLE DB" 209 | # oracle.ntoledb.odp_net_2:19.0.0.0.0 -- "Oracle Data Provider for .NET" 210 | # oracle.aspnet_2:19.0.0.0.0 -- "Oracle Providers for ASP.NET" 211 | # 212 | # Example : oracle.install.client.customComponents="oracle.precomp:19.0.0.0.0","oracle.oraolap.mgmt:19.0.0.0.0","oracle.rdbms.scheduler:19.0.0.0.0" 213 | #------------------------------------------------------------------------------- 214 | 215 | 216 | #> 217 | 218 | Invoke-LabCommand -ComputerName $clientVMs.Name -ActivityName 'Installing Oracle Client' -ScriptBlock { 219 | $rspContent = @( 220 | 'ORACLE_BASE=C:\oracle' 221 | 'ORACLE_HOME=C:\oracle\product\19.0.0\client_1' 222 | 'oracle.install.responseFileVersion=/oracle/install/rspfmt_clientinstall_response_schema_v19.0.0' 223 | 'oracle.install.IsBuiltInAccount=true' 224 | 'oracle.install.client.installType=Custom' 225 | 'oracle.install.client.customComponents=oracle.ntoledb.odp_net_2:19.0.0.0.0,oracle.sqlplus:19.0.0.0.0,oracle.rdbms.util:19.0.0.0.0' 226 | ) 227 | $argumentList = @( 228 | '-silent' 229 | '-responseFile C:\Software\OracleClientInstall.rsp' 230 | '-noConsole' 231 | ) 232 | Expand-Archive -Path C:\Software\WINDOWS.X64_193000_client.zip -DestinationPath C:\Software\WINDOWS.X64_193000_client 233 | $rspContent | Set-Content -Path C:\Software\OracleClientInstall.rsp 234 | Start-Process -FilePath C:\Software\WINDOWS.X64_193000_client\client\setup.exe -ArgumentList $argumentList -Wait 235 | } 236 | 237 | 238 | if ($ChocolateyPackages.Count -gt 0) { 239 | Invoke-LabCommand -ComputerName $allVMs.Name -ActivityName 'Installing chocolatey packages' -ArgumentList @(, $ChocolateyPackages) -ScriptBlock { 240 | param($ChocolateyPackages) 241 | 242 | $ErrorActionPreference = 'Stop' 243 | 244 | $logPath = 'C:\DeployDebug\InstallChocolateyPackages.log' 245 | 246 | try { 247 | Invoke-Expression -Command ([System.Net.WebClient]::new().DownloadString('https://chocolatey.org/install.ps1')) *>$logPath 248 | $installResult = choco install $ChocolateyPackages --confirm --limitoutput --no-progress *>&1 249 | if ($installResult -match 'Warnings:') { 250 | Write-Warning -Message 'Chocolatey generated warnings' 251 | } 252 | $info = $installResult -match 'Chocolatey installed (\d+)/(\d+) packages' | Select-Object -First 1 253 | if ($info -match 'Chocolatey installed (\d+)/(\d+) packages') { 254 | if ($Matches[1] -ne $Matches[2]) { 255 | Write-Warning -Message "Chocolatey only installed $($Matches[1]) of $($Matches[2]) packages" 256 | $installResult | Add-Content -Path $logPath 257 | } 258 | } else { 259 | Write-Warning -Message "InstallResult: $installResult" 260 | } 261 | } catch { 262 | $message = "Setting up Chocolatey failed: $_" 263 | $message | Add-Content -Path $logPath 264 | Write-Warning -Message $message 265 | } 266 | } 267 | } 268 | 269 | 270 | if ($PowerShellModules.Count -gt 0) { 271 | Invoke-LabCommand -ComputerName $allVMs.Name -ActivityName 'Installing PowerShell modules' -ArgumentList @(, $PowerShellModules) -ScriptBlock { 272 | param($PowerShellModules) 273 | 274 | $logPath = 'C:\DeployDebug\InstallPowerShellModules.log' 275 | 276 | $ErrorActionPreference = 'Stop' 277 | 278 | try { 279 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 280 | if ((Get-PackageProvider -ListAvailable).Name -notcontains 'Nuget') { 281 | $null = Install-PackageProvider -Name Nuget -Force 282 | 'Install-PackageProvider ok' | Add-Content -Path $logPath 283 | } else { 284 | 'Install-PackageProvider not needed' | Add-Content -Path $logPath 285 | } 286 | if ((Get-PSRepository -Name PSGallery).InstallationPolicy -ne 'Trusted') { 287 | Set-PSRepository -Name PSGallery -InstallationPolicy Trusted 288 | 'Set-PSRepository ok' | Add-Content -Path $logPath 289 | } else { 290 | 'Set-PSRepository not needed' | Add-Content -Path $logPath 291 | } 292 | foreach ($name in $PowerShellModules) { 293 | if (-not (Get-Module -Name $name -ListAvailable)) { 294 | Install-Module -Name $name 295 | "Install-Module $name ok" | Add-Content -Path $logPath 296 | } else { 297 | "Install-Module $name not needed" | Add-Content -Path $logPath 298 | } 299 | } 300 | } catch { 301 | $message = "Setting up PowerShell failed: $_" 302 | $message | Add-Content -Path $logPath 303 | Write-Warning -Message $message 304 | } 305 | } 306 | } 307 | 308 | -------------------------------------------------------------------------------- /AutomatedLab/CustomScripts/Win2022_SQLServer.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Continue' 2 | 3 | Import-Module -Name AutomatedLab 4 | 5 | $LabName = "WinSql" 6 | $LabNetworkBase = '192.168.113' 7 | $LabDnsServer = '1.1.1.1' 8 | 9 | $LabAdminUser = 'User' 10 | $LabAdminPassword = 'Passw0rd!' 11 | 12 | $MachineDefinition = @{ 13 | Name = 'SQLServer' 14 | OperatingSystem = 'Windows Server 2022 Standard Evaluation (Desktop Experience)' 15 | Memory = 4GB 16 | Processors = 4 17 | Network = $LabName 18 | IpAddress = "$LabNetworkBase.10" 19 | Gateway = "$LabNetworkBase.1" 20 | DnsServer1 = $LabDnsServer 21 | TimeZone = 'W. Europe Standard Time' 22 | } 23 | 24 | $CopySoftware = @( 25 | "$labSources\CustomAssets\Software\SQLEXPR_x64_ENU.exe" # SQL Server Express from: https://www.microsoft.com/en-US/download/details.aspx?id=101064 26 | "$labSources\CustomAssets\Software\SQLServer2019-KB5017593-x64.exe" # SQL Server 2019 CU 18 27 | ) 28 | 29 | 30 | <# Some commands that I use for importing, removing, stopping, starting or connecting to the lab: 31 | 32 | Import-Lab -Name $LabName -NoValidation 33 | 34 | Remove-Lab -Name $LabName -Confirm:$false; Get-NetNat -Name $LabName -ErrorAction SilentlyContinue | Remove-NetNat -Confirm:$false 35 | 36 | Stop-VM -Name $MachineDefinition.Name 37 | Start-VM -Name $MachineDefinition.Name 38 | 39 | #> 40 | 41 | 42 | ### End of configuration ### 43 | 44 | 45 | New-LabDefinition -Name $LabName -DefaultVirtualizationEngine HyperV 46 | Set-LabInstallationCredential -Username $LabAdminUser -Password $LabAdminPassword 47 | Add-LabVirtualNetworkDefinition -Name $LabName -AddressSpace "$LabNetworkBase.0/24" 48 | Add-LabMachineDefinition @MachineDefinition 49 | Install-Lab -NoValidation 50 | 51 | $null = New-NetNat -Name $LabName -InternalIPInterfaceAddressPrefix "$LabNetworkBase.0/24" 52 | 53 | Invoke-LabCommand -ComputerName $MachineDefinition.Name -ActivityName 'Disable Windows updates' -ScriptBlock { 54 | # https://learn.microsoft.com/en-us/windows/deployment/update/waas-wu-settings 55 | Set-ItemProperty -Path HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU -Name NoAutoUpdate -Value 1 56 | } 57 | 58 | Invoke-LabCommand -ComputerName $MachineDefinition.Name -ActivityName 'Setting my favorite explorer settings' -ScriptBlock { 59 | Set-ItemProperty -Path HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced -Name HideFileExt -Value 0 60 | Set-ItemProperty -Path HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced -Name NavPaneShowAllFolders -Value 1 61 | Set-ItemProperty -Path HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced -Name NavPaneExpandToCurrentFolder -Value 1 62 | } 63 | 64 | foreach ($file in $CopySoftware) { 65 | Copy-LabFileItem -Path $file -ComputerName $MachineDefinition.Name -DestinationFolderPath C:\Software 66 | } 67 | 68 | Invoke-LabCommand -ComputerName $MachineDefinition.Name -ActivityName 'Installing SQL Server' -ArgumentList $LabAdminPassword -ScriptBlock { 69 | param($Password) 70 | $argumentList = @( 71 | '/x:C:\Software\SQLEXPR_x64_ENU' 72 | '/q' 73 | '/IACCEPTSQLSERVERLICENSETERMS' 74 | '/ACTION=INSTALL' 75 | '/UpdateEnabled=True' 76 | '/UpdateSource=C:\Software' 77 | '/FEATURES=SQL' 78 | '/INSTANCENAME=SQLEXPRESS' 79 | '/SECURITYMODE=SQL' 80 | "/SAPWD=$Password" 81 | '/TCPENABLED=1' 82 | '/SQLSVCINSTANTFILEINIT=True' 83 | ) 84 | Start-Process -FilePath C:\Software\SQLEXPR_x64_ENU.exe -ArgumentList $argumentList -Wait 85 | } 86 | 87 | -------------------------------------------------------------------------------- /AutomatedLab/README.md: -------------------------------------------------------------------------------- 1 | # AutomatedLab 2 | 3 | Information and resources for using the PowerShell module AutomatedLab (https://automatedlab.org/) to set up labs. 4 | 5 | This is a modified copy of a private repository, so some things might not work without further modification. All information and scripts are intended as inspiration for own developments. 6 | 7 | 8 | ## Installation 9 | 10 | All commands must be executed in an administrative PowerShell. 11 | 12 | For installation, the commands from the scripts in this directory can simply be copied into a PowerShell ISE and executed there line by line or block by block. 13 | 14 | 15 | ### 01_Install_HyperV.ps1 16 | 17 | In advance, the ExecutionPolicy is tested here and set to RemoteSigned if necessary. 18 | 19 | The Windows Hyper-V function is then installed and the computer is restarted. If Hyper-V is already in use, this step can be skipped. 20 | 21 | 22 | ### 02_Install_AutomatedLab.ps1 23 | 24 | First, PSGallery is set up as a trusted source for PowerShell packages. Since Microsoft servers only support the current TLS 1.2 protocol, the commented out line may still need to be used on older operating systems. On current operating systems, however, this should no longer be necessary. If errors occur, this may be due to the temporary inaccessibility of the PSGallery. Then some time later a new attempt has to be started. 25 | 26 | In addition to the PowerShell module AutomatedLab, Posh-SSH is also installed to enable convenient access to the Linux machines via SSH. The additional options during the installation of AutomatedLab are necessary, details are described [here](https://automatedlab.org/en/latest/Wiki/Basic/install/). 27 | 28 | The environment variable AUTOMATEDLAB_TELEMETRY_OPTIN controls whether a corresponding information should be sent to the developers when setting up and dismantling labs. We want to switch this off in our case. 29 | 30 | Deviating from the standard configuration, two configurations are changed to adjust the paths used. These are my recommendations, but anyone can deviate from them. 31 | 32 | The configuration "LabSourcesLocation" is later always accessible under the global variable `$labSources` and is also referenced in this way in all scripts. In this path above all the ISOs of the used operating systems are stored, which are needed only during the construction of the labs. Additional components and examples are also stored here. Also the ISOs of the SQL Servers and installation media of Oracle are stored later under this path. The default is only "LabSources" and offers thus first no reference to the used program, therefore the adjustment. 33 | 34 | The "VmPath" configuration controls the location of the created Hyper-V machines. Without this configuration, the fastest drive is automatically used and the directory is created there. 35 | 36 | After setting the configuration, the module is loaded and initialized. During this process, additional files are loaded from the internet. 37 | 38 | Since the module itself does not provide any operating system ISOs, you have to obtain them yourself. I recommend using the ISOs provided by Microsoft with evaluation versions of the operating systems here. The client operating systems Windows 10 and 11 have a test period of 90 days, the server operating system Windows Server 2022 has a test period of 180 days. In addition, the deployment of Windows Server 2022 is a lot faster than the deployment of Windows 10 or 11, which is why I only use Windows Server 2022 in my labs myself. The Linux variants specified here can be installed automatically, newer versions are not currently supported. I mainly use CentOS-7, because the installation is finished faster than with CentOS-Stream-9 or openSUSE-Leap-15.2. 39 | 40 | 41 | ### 03_Setup_TestLab.ps1 42 | 43 | At this point you can build a first lab to test the functionality. 44 | 45 | The lab can be configured in a few places: Firstly, the creation of the domain can be switched off to shorten the installation time. Only the two machines "Windows" and "Linux" are then set up. On the other hand the used DNS server can be fixed, because the automatic determination could not be tested sufficiently. However, the DNS server setting is only used in the configuration without domain, because with domain the DC takes over the role of the DNS proxy. In addition, the Chocolatey packages, PowerShell modules and Docker containers installed for testing can be customized. 46 | 47 | 48 | ### 04_Download_CustomAssets.ps1 49 | 50 | The script downloads this repository and moves the "CustomAssets" and "CustomScripts" directories to the "$labSources" directory. Directly contained there are currently only the backups of GPOs that I regularly use in my labs. There are also subdirectories for other files, but I don't want to include these files in the repository here. I only provide a list of files that I use internally. 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Andreas Jordan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This is my demo space 2 | 3 | This is mainly for demos with Microsoft SQL Server and PowerShell. And very much work in progress all the time. 4 | 5 | 6 | ## Setup a lab environment for demos 7 | 8 | I switched from PSAutoLab to AutomatedLab end of 2022, so you now find my scripts and resources to setup my lab in the AutomatedLab folder. Keep in mind that this is a modified copy of a private repository, so some things might not work without further modification. All information and scripts are intended as inspiration for own developments. 9 | 10 | 11 | ## Setup Always On with dbatools 12 | 13 | In the folder AlwaysOn you find scripts to setup a cluster, default instances and Always On Availability Groups with the PowerShell module [dbatools](https://dbatools.io/). 14 | In the folder AlwaysOn_MultiInstance the demo is extended to use named instances with various SQL Server versions. 15 | 16 | 17 | ## Output results of sp_Blitz in Markdown format 18 | 19 | In the folder sp_Blitz_markdown you find a script that converts the results of sp_Blitz in a bulleted list in Markdown format. 20 | 21 | -------------------------------------------------------------------------------- /SQLServer/Fragmentation.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Part 1: 3 | 4 | Overall view of all pages related to user databases. 5 | 6 | Can be be executed from any database context 7 | and shows information about every user database, 8 | but object names cannot be retrieved. 9 | */ 10 | 11 | 12 | /* 13 | Show one line per database. 14 | */ 15 | SELECT database_id 16 | , DB_NAME(database_id) AS database_name 17 | , COUNT(*) AS pages 18 | , COUNT(*)*8/1024 AS mb_total 19 | , SUM(CAST(free_space_in_bytes AS bigint))/1024/1024 AS mb_free 20 | FROM sys.dm_os_buffer_descriptors 21 | WHERE database_id BETWEEN 5 and 32760 22 | GROUP BY database_id 23 | ORDER BY SUM(CAST(free_space_in_bytes AS bigint)); 24 | 25 | 26 | /* 27 | Show one line per database, (relevant) page type and page level. 28 | */ 29 | SELECT database_id 30 | , DB_NAME(database_id) AS database_name 31 | , page_type 32 | , page_level 33 | , COUNT(*) AS pages 34 | , COUNT(*)*8/1024 AS mb_total 35 | , SUM(CAST(free_space_in_bytes AS bigint))/1024/1024 AS mb_free 36 | FROM sys.dm_os_buffer_descriptors 37 | WHERE database_id BETWEEN 5 and 32760 38 | GROUP BY database_id 39 | , page_type 40 | , page_level 41 | -- comment out to see all page types: 42 | HAVING page_type NOT IN ('BOOT_PAGE', 'DIFF_MAP_PAGE', 'FILEHEADER_PAGE', 'GAM_PAGE', 'SGAM_PAGE', 'PFS_PAGE') 43 | ORDER BY database_id 44 | , page_type 45 | , page_level; 46 | 47 | 48 | /* 49 | Show one line per database and custom page type group. 50 | */ 51 | WITH data AS ( 52 | SELECT database_id 53 | , DB_NAME(database_id) AS database_name 54 | , CASE 55 | WHEN page_type = 'DATA_PAGE' 56 | THEN 'DATA_PAGE' 57 | WHEN page_type = 'INDEX_PAGE' 58 | AND page_level = 0 59 | THEN 'INDEX_PAGE_leaf' 60 | WHEN page_type = 'INDEX_PAGE' 61 | AND page_level > 0 62 | THEN 'INDEX_PAGE_non_leaf' 63 | ELSE 'other_page' 64 | END AS page_type_and_level 65 | , CASE 66 | WHEN page_type = 'DATA_PAGE' 67 | THEN 1 68 | WHEN page_type = 'INDEX_PAGE' 69 | AND page_level = 0 70 | THEN 2 71 | WHEN page_type = 'INDEX_PAGE' 72 | AND page_level > 0 73 | THEN 3 74 | ELSE 4 75 | END AS order_id 76 | , CAST(free_space_in_bytes AS bigint) AS free_space_in_bytes 77 | FROM sys.dm_os_buffer_descriptors 78 | WHERE database_id BETWEEN 5 and 32760 79 | ) 80 | SELECT database_id 81 | , database_name 82 | , page_type_and_level 83 | , COUNT(*) AS pages 84 | , COUNT(*)*8/1024 AS mb_total 85 | , SUM(free_space_in_bytes)/1024/1024 AS mb_free 86 | FROM data 87 | GROUP BY database_id 88 | , database_name 89 | , page_type_and_level 90 | , order_id 91 | ORDER BY database_id 92 | , order_id; 93 | 94 | 95 | /* 96 | Show one line per database and custom page type group. 97 | Like the previous statement, but we now focus on fragmentation. 98 | Filtering out "other_page" and small mb_free. 99 | Sorting by mb_free descending. 100 | */ 101 | WITH data AS ( 102 | SELECT database_id 103 | , DB_NAME(database_id) AS database_name 104 | , CASE 105 | WHEN page_type = 'DATA_PAGE' 106 | THEN 'DATA_PAGE' 107 | WHEN page_type = 'INDEX_PAGE' 108 | AND page_level = 0 109 | THEN 'INDEX_PAGE_leaf' 110 | ELSE 'INDEX_PAGE_non_leaf' 111 | END AS page_type_and_level 112 | , CAST(free_space_in_bytes AS bigint) AS free_space_in_bytes 113 | FROM sys.dm_os_buffer_descriptors 114 | WHERE database_id BETWEEN 5 and 32760 115 | AND page_type IN ('DATA_PAGE', 'INDEX_PAGE') 116 | ) 117 | SELECT database_id 118 | , database_name 119 | , page_type_and_level 120 | , COUNT(*) AS pages 121 | , COUNT(*)*8/1024 AS mb_total 122 | , SUM(free_space_in_bytes)/1024/1024 AS mb_free 123 | FROM data 124 | GROUP BY database_id 125 | , database_name 126 | , page_type_and_level 127 | HAVING SUM(free_space_in_bytes)/1024/1024 > 100 128 | ORDER BY SUM(free_space_in_bytes) DESC; 129 | 130 | 131 | 132 | /* 133 | Part 2: 134 | 135 | Detailed view of all pages related to one database. 136 | 137 | Must be be executed from the target database context 138 | and shows information per object. 139 | */ 140 | 141 | USE test01 142 | GO 143 | 144 | 145 | /* 146 | Show detailed information for "DATA_PAGE" and "INDEX_PAGE_leaf" per index on user objects. 147 | */ 148 | SELECT DB_ID() AS database_id 149 | , DB_NAME() AS database_name 150 | , o.name AS table_name 151 | , i.index_id 152 | , i.name AS index_name 153 | , i.type_desc AS index_type 154 | , CASE 155 | WHEN bd.page_type = 'DATA_PAGE' 156 | THEN 'DATA_PAGE' 157 | ELSE 'INDEX_PAGE_leaf' 158 | END AS page_type_and_level 159 | , COUNT(*) AS pages 160 | , COUNT(*)*8/1024 AS mb_total 161 | , SUM(CAST(bd.free_space_in_bytes AS bigint))/1024/1024 AS mb_free 162 | , AVG(CAST(bd.free_space_in_bytes AS bigint)) AS avg_free_space_in_bytes 163 | , (8096-AVG(CAST(bd.free_space_in_bytes AS bigint)))*100/8096 AS avg_page_space_used_in_percent 164 | , CASE 165 | WHEN i.fill_factor = 0 166 | THEN 100 167 | ELSE i.fill_factor 168 | END AS index_fill_factor 169 | FROM sys.dm_os_buffer_descriptors AS bd 170 | JOIN sys.allocation_units AS au ON bd.allocation_unit_id = au.allocation_unit_id 171 | JOIN sys.partitions AS p ON au.container_id = p.hobt_id 172 | JOIN sys.objects AS o ON p.object_id = o.object_id 173 | JOIN sys.indexes AS i ON p.object_id = i.object_id AND p.index_id = i.index_id 174 | WHERE bd.database_id = DB_ID() 175 | AND bd.page_type IN ('DATA_PAGE', 'INDEX_PAGE') 176 | AND bd.page_level = 0 177 | AND au.type_desc = 'IN_ROW_DATA' 178 | AND o.is_ms_shipped = 0 179 | GROUP BY o.name 180 | , i.index_id 181 | , i.name 182 | , i.type_desc 183 | , i.fill_factor 184 | , bd.page_type 185 | -- Sort order 1: By table_name and index_id 186 | ORDER BY o.name, i.index_id; 187 | -- Sort order 2: By mb_free descending 188 | ORDER BY SUM(CAST(bd.free_space_in_bytes AS bigint)) DESC; 189 | 190 | 191 | /* 192 | Show detailed information of all pages not shown in previous query. 193 | */ 194 | SELECT DB_ID() AS database_id 195 | , DB_NAME() AS database_name 196 | , o.is_ms_shipped 197 | , o.name AS table_name 198 | , i.index_id 199 | , i.name AS index_name 200 | , i.type_desc AS index_type 201 | , bd.page_type AS page_type 202 | , bd.page_level 203 | , au.type_desc AS allocation_type 204 | , COUNT(*) AS pages 205 | , COUNT(*)*8/1024 AS mb_total 206 | FROM sys.dm_os_buffer_descriptors AS bd 207 | LEFT JOIN sys.allocation_units AS au ON bd.allocation_unit_id = au.allocation_unit_id 208 | LEFT JOIN sys.partitions AS p ON au.container_id = p.hobt_id 209 | LEFT JOIN sys.objects AS o ON p.object_id = o.object_id 210 | LEFT JOIN sys.indexes AS i ON p.object_id = i.object_id AND p.index_id = i.index_id 211 | WHERE bd.database_id = DB_ID() 212 | AND NOT ( ISNULL(bd.page_type, 'X') IN ('DATA_PAGE', 'INDEX_PAGE') 213 | AND ISNULL(bd.page_level, -1) = 0 214 | AND ISNULL(au.type_desc, 'X') = 'IN_ROW_DATA' 215 | AND ISNULL(o.is_ms_shipped, -1) = 0 216 | ) 217 | GROUP BY o.is_ms_shipped 218 | , o.name 219 | , i.index_id 220 | , i.name 221 | , i.type_desc 222 | , bd.page_type 223 | , bd.page_level 224 | , au.type_desc 225 | ORDER BY 1,2,3,4,5,6,7,8,9,10; 226 | 227 | 228 | /* 229 | Very detailed view, query should be customized for individual needs. 230 | */ 231 | SELECT DB_ID() AS database_id 232 | , DB_NAME() AS database_name 233 | , o.name AS table_name 234 | , i.name AS index_name 235 | , i.type_desc AS index_type 236 | , i.fill_factor 237 | , bd.page_type 238 | , au.type_desc AS allocation_type 239 | , bd.file_id 240 | , bd.page_id 241 | , bd.row_count 242 | , bd.free_space_in_bytes 243 | , bd.page_level 244 | FROM sys.dm_os_buffer_descriptors AS bd 245 | JOIN sys.allocation_units AS au ON bd.allocation_unit_id = au.allocation_unit_id 246 | JOIN sys.partitions AS p ON au.container_id = p.hobt_id 247 | JOIN sys.objects AS o ON p.object_id = o.object_id 248 | JOIN sys.indexes AS i ON p.object_id = i.object_id AND p.index_id = i.index_id 249 | WHERE bd.database_id = DB_ID() 250 | -- AND bd.page_type IN ('DATA_PAGE', 'INDEX_PAGE') 251 | -- AND bd.page_level = 0 252 | -- AND au.type_desc = 'IN_ROW_DATA' 253 | -- AND o.is_ms_shipped = 0 254 | ORDER BY 1,2,3,4,5,6,7,8,9; 255 | 256 | -------------------------------------------------------------------------------- /SQLServer/Fragmentation_createDemoData.sql: -------------------------------------------------------------------------------- 1 | USE master 2 | GO 3 | 4 | DROP DATABASE TestDB 5 | GO 6 | 7 | CREATE DATABASE TestDB 8 | GO 9 | 10 | ALTER DATABASE TestDB SET RECOVERY SIMPLE; 11 | GO 12 | 13 | USE TestDB 14 | GO 15 | 16 | SET NOCOUNT ON; 17 | GO 18 | 19 | CREATE TABLE dbo.TestTableA 20 | ( Id int IDENTITY 21 | , JustSpace varchar(300) DEFAULT REPLICATE('Space', 30) -- 5x 30 = 150 BYTE 22 | , CONSTRAINT TestTableA_PK PRIMARY KEY (Id) 23 | ); 24 | GO 25 | 26 | CREATE TABLE dbo.TestTableB 27 | ( Id int IDENTITY 28 | , JustSpace varchar(300) DEFAULT REPLICATE('Space', 40) -- 5x 40 = 200 BYTE 29 | , CONSTRAINT TestTableB_PK PRIMARY KEY (Id) 30 | ); 31 | GO 32 | 33 | CREATE TABLE dbo.TestTableC 34 | ( Id int IDENTITY 35 | , JustSpace varchar(300) DEFAULT REPLICATE('Space', 60) -- 5x 60 = 300 BYTE 36 | , CONSTRAINT TestTableC_PK PRIMARY KEY (Id) 37 | ); 38 | GO 39 | 40 | INSERT INTO dbo.TestTableA DEFAULT VALUES; 41 | INSERT INTO dbo.TestTableB DEFAULT VALUES; 42 | INSERT INTO dbo.TestTableC DEFAULT VALUES; 43 | GO 100000 44 | 45 | UPDATE dbo.TestTableA 46 | SET JustSpace = JustSpace + 'we need much more Space!!we need much more Space!!' -- 150 -> 200 BYTE 47 | WHERE Id <= 30000; 48 | 49 | UPDATE dbo.TestTableB 50 | SET JustSpace = SUBSTRING(JustSpace, 1, 150) -- 200 -> 150 BYTE 51 | WHERE Id > 30000; 52 | 53 | UPDATE dbo.TestTableC 54 | SET JustSpace = SUBSTRING(JustSpace, 1, 200) -- 300 -> 200 BYTE 55 | WHERE Id <= 30000; 56 | 57 | UPDATE dbo.TestTableC 58 | SET JustSpace = SUBSTRING(JustSpace, 1, 150) -- 300 -> 150 BYTE 59 | WHERE Id > 30000; 60 | 61 | SELECT OBJECT_NAME(object_id) table_name, index_id, avg_fragmentation_in_percent, fragment_count, page_count, avg_page_space_used_in_percent, avg_record_size_in_bytes, record_count 62 | FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'DETAILED') 63 | WHERE OBJECT_NAME(object_id) LIKE 'Test%' 64 | AND index_level = 0; 65 | 66 | /* 67 | TestTableA 1 46,32 1437 2709 82,98 180 100000 68 | TestTableB 1 0,33 347 2703 83,16 180 100000 69 | TestTableC 1 0,35 434 4000 56,19 180 100000 70 | 71 | Test table A and B have both filled their pages to around 80 percent, but only A has a high value in avg_fragmentation_in_percent and would be processed by most de-fragmentation tools. 72 | Test table C has their pages only filled around 50 percent, but as the value of avg_fragmentation_in_percent is so low, it would not be processed by most de-fragmentation tools. 73 | */ 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | USE master 83 | GO 84 | 85 | DROP DATABASE TestDB 86 | GO 87 | 88 | CREATE DATABASE TestDB 89 | GO 90 | 91 | ALTER DATABASE TestDB SET RECOVERY SIMPLE; 92 | GO 93 | 94 | USE TestDB 95 | GO 96 | 97 | SET NOCOUNT ON; 98 | GO 99 | 100 | CREATE TABLE dbo.TestTableA 101 | ( Id int IDENTITY 102 | , Number decimal(10,10) DEFAULT RAND() 103 | , String char(30) DEFAULT SUBSTRING(CONVERT(varchar, RAND()*10000000, 3), 3, 15) + SUBSTRING(CONVERT(varchar, RAND()*10000000, 3), 3, 15) 104 | , JustSpace varchar(300) DEFAULT REPLICATE('Space', 30) -- 5x 30 = 150 BYTE 105 | , CONSTRAINT TestTableA_PK PRIMARY KEY (Id) 106 | ); 107 | GO 108 | 109 | INSERT INTO dbo.TestTableA DEFAULT VALUES; 110 | GO 95000 111 | 112 | CREATE INDEX TestIndex ON dbo.TestTableA (String) WITH FILLFACTOR = 90 113 | GO 114 | 115 | SELECT OBJECT_NAME(object_id) table_name, index_id, avg_fragmentation_in_percent, fragment_count, page_count, avg_page_space_used_in_percent, avg_record_size_in_bytes, record_count 116 | FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'DETAILED') 117 | WHERE OBJECT_NAME(object_id) LIKE 'Test%' 118 | AND index_id = 2 119 | AND index_level = 0; 120 | GO 121 | 122 | INSERT INTO dbo.TestTableA DEFAULT VALUES; 123 | GO 5000 124 | DELETE dbo.TestTableA WHERE Id < (SELECT MIN(Id) FROM dbo.TestTableA) + 5000 125 | GO 126 | SELECT OBJECT_NAME(object_id) table_name, index_id, avg_fragmentation_in_percent, fragment_count, page_count, avg_page_space_used_in_percent, avg_record_size_in_bytes, record_count 127 | FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'DETAILED') 128 | WHERE OBJECT_NAME(object_id) LIKE 'Test%' 129 | AND index_id = 2 130 | AND index_level = 0; 131 | GO 132 | 133 | INSERT INTO dbo.TestTableA DEFAULT VALUES; 134 | GO 5000 135 | DELETE dbo.TestTableA WHERE Id < (SELECT MIN(Id) FROM dbo.TestTableA) + 5000 136 | GO 137 | SELECT OBJECT_NAME(object_id) table_name, index_id, avg_fragmentation_in_percent, fragment_count, page_count, avg_page_space_used_in_percent, avg_record_size_in_bytes, record_count 138 | FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'DETAILED') 139 | WHERE OBJECT_NAME(object_id) LIKE 'Test%' 140 | AND index_id = 2 141 | AND index_level = 0; 142 | GO 143 | 144 | INSERT INTO dbo.TestTableA DEFAULT VALUES; 145 | GO 5000 146 | DELETE dbo.TestTableA WHERE Id < (SELECT MIN(Id) FROM dbo.TestTableA) + 5000 147 | GO 148 | SELECT OBJECT_NAME(object_id) table_name, index_id, avg_fragmentation_in_percent, fragment_count, page_count, avg_page_space_used_in_percent, avg_record_size_in_bytes, record_count 149 | FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'DETAILED') 150 | WHERE OBJECT_NAME(object_id) LIKE 'Test%' 151 | AND index_id = 2 152 | AND index_level = 0; 153 | GO 154 | 155 | INSERT INTO dbo.TestTableA DEFAULT VALUES; 156 | GO 5000 157 | DELETE dbo.TestTableA WHERE Id < (SELECT MIN(Id) FROM dbo.TestTableA) + 5000 158 | GO 159 | SELECT OBJECT_NAME(object_id) table_name, index_id, avg_fragmentation_in_percent, fragment_count, page_count, avg_page_space_used_in_percent, avg_record_size_in_bytes, record_count 160 | FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'DETAILED') 161 | WHERE OBJECT_NAME(object_id) LIKE 'Test%' 162 | AND index_id = 2 163 | AND index_level = 0; 164 | GO 165 | 166 | INSERT INTO dbo.TestTableA DEFAULT VALUES; 167 | GO 5000 168 | DELETE dbo.TestTableA WHERE Id < (SELECT MIN(Id) FROM dbo.TestTableA) + 5000 169 | GO 170 | 171 | SELECT OBJECT_NAME(object_id) table_name, index_id, avg_fragmentation_in_percent, fragment_count, page_count, avg_page_space_used_in_percent, avg_record_size_in_bytes, record_count 172 | FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'DETAILED') 173 | WHERE OBJECT_NAME(object_id) LIKE 'Test%' 174 | AND index_id = 2 175 | AND index_level = 0; 176 | GO 177 | 178 | --ALTER INDEX TestIndex ON dbo.TestTableA REBUILD 179 | --GO 180 | 181 | DELETE dbo.TestTableA WHERE Id < (SELECT MIN(Id) FROM dbo.TestTableA) + 25000 182 | GO 183 | 184 | SELECT OBJECT_NAME(object_id) table_name, index_id, avg_fragmentation_in_percent, fragment_count, page_count, avg_page_space_used_in_percent, avg_record_size_in_bytes, record_count 185 | FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'DETAILED') 186 | WHERE OBJECT_NAME(object_id) LIKE 'Test%' 187 | AND index_id = 2 188 | AND index_level = 0; 189 | GO 190 | 191 | 192 | 193 | 194 | --SELECT * FROM dbo.TestTableA; 195 | 196 | --UPDATE dbo.TestTableA SET String = REVERSE(String) WHERE Number < 0.3 197 | 198 | -------------------------------------------------------------------------------- /dbatools/Get-CU.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [ValidateSet('2022', '2019', '2017', '2016', '2014')] 4 | [string[]]$Version = @('2022', '2019', '2017'), 5 | [int]$Last = 1, 6 | [string]$Path = '.', 7 | [switch]$UpdateBuildReference = $true 8 | ) 9 | 10 | function Get-CU { 11 | param( 12 | [Parameter(Mandatory)] 13 | [ValidateSet('2022', '2019', '2017', '2016', '2014')] 14 | [string]$Version, 15 | [regex]$BuildBaseRegex, 16 | [int]$Last = 1, 17 | [string[]]$Exclude, 18 | [string]$Path = '.' 19 | ) 20 | if ($null -eq $BuildBaseRegex) { 21 | $BuildBaseRegex = switch ($Version) { 22 | '2022' { '^16' } 23 | '2019' { '^15' } 24 | '2017' { '^14' } 25 | '2016' { '^13.0.5' } # Based on SP2 26 | '2014' { '^12.0.6' } # Based on SP3 27 | } 28 | } 29 | if ($Version -eq '2019') { 30 | $Exclude += 'CU7' # CU7 is not available any more 31 | } 32 | $buildrefFile = Join-Path -Path (Get-DbatoolsConfigValue -Name 'Path.DbatoolsData') -ChildPath "dbatools-buildref-index.json" 33 | $buildrefData = (Get-Content -Path $buildrefFile -Raw | ConvertFrom-Json).Data 34 | $cuData = $buildrefData | 35 | Where-Object -FilterScript { $_.Version -match $BuildBaseRegex -and $_.CU -ne $null -and $_.CU -notin $Exclude } | 36 | Sort-Object -Property KBList | 37 | Select-Object -Last $Last 38 | foreach ($cu in $cuData) { 39 | $kbNr = $cu.KBList 40 | $cuName = $cu.CU 41 | $filePath = Join-Path -Path $Path -ChildPath "SQLServer$Version-KB$kbNr-$cuName-x64.exe" 42 | if (-not (Test-Path -Path $filePath)) { 43 | Write-Progress -Activity "Downloading Cumulative Updates for SQL Server" -Status "Downloading KB $kbNr for SQL Server $Version to $filePath" 44 | Save-DbaKbUpdate -Name $kbNr -FilePath $filePath 45 | } 46 | } 47 | } 48 | 49 | Write-Progress -Activity "Downloading Cumulative Updates for SQL Server" -Status "Importing dbatools" 50 | Import-Module -Name dbatools 6> $null 51 | 52 | if ($UpdateBuildReference) { 53 | Write-Progress -Activity "Downloading Cumulative Updates for SQL Server" -Status "Updating build reference" 54 | Update-DbaBuildReference 55 | } 56 | 57 | foreach ($ver in $Version) { 58 | Get-CU -Version $ver -Last $Last -Path $Path 59 | } 60 | 61 | Write-Progress -Activity "Downloading Cumulative Updates for SQL Server" -Completed 62 | 63 | 64 | <# Other usage examples: 65 | 66 | Get-CU -Version 2019 -Path 'C:\SQLServerPatches' 67 | Get-CU -Version 2017 -Path 'C:\SQLServerPatches' -Last 5 68 | Get-CU -Version 2016 -Path 'C:\SQLServerPatches' -Last 5 69 | Get-CU -Version 2014 -Path 'C:\SQLServerPatches' 70 | # KB4583462 - Security update for SQL Server 2014 SP3 CU4: January 12, 2021 71 | Save-DbaKbUpdate -Name 4583462 -FilePath 'C:\SQLServerPatches\SQLServer2014-KB4583462-CU4-Security-x64.exe' 72 | # KB4583465 - Security update for SQL Server 2012 SP4 GDR: January 12, 2021 73 | Save-DbaKbUpdate -Name 4583465 -FilePath 'C:\SQLServerPatches\SQLServer2012-KB4583465-GDR-Security-x64.exe' 74 | # Check for new versions at: https://sqlserverbuilds.blogspot.com/ 75 | # KB5003830 - New CU that is not yet known by dbatools 76 | Save-DbaKbUpdate -Name 5003830 -FilePath 'C:\SQLServerPatches\SQLServer2017-KB5003830-CU25-x64.exe' 77 | 78 | #> 79 | -------------------------------------------------------------------------------- /dbatools/downgrade_database.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | 3 | This demo shows how to downgrade the database StackOverflowMini from version 2022 to an earlier version. 4 | 5 | Get the database from here: https://github.com/BrentOzarULTD/Stack-Overflow-Database 6 | 7 | This could also help you downgrade other databases, but changes may be required. 8 | 9 | #> 10 | 11 | 12 | Import-Module -Name dbatools 13 | 14 | # Connect to the source database. 15 | # You may have to add -SqlCredential if using integrated security is not possible. 16 | $sourceDatabase = Get-DbaDatabase -SqlInstance SQL01\SQL2022 -Database StackOverflowMini 17 | 18 | # Connect to the target instance. 19 | # You may have to add -SqlCredential if using integrated security is not possible. 20 | $targetServer = Connect-DbaInstance -SqlInstance SQL01\SQL2019 21 | 22 | # Define the parameters for the target database based on the source database. 23 | $targetDatabaseParams = @{ 24 | SqlInstance = $targetServer 25 | Name = $sourceDatabase.Name 26 | Collation = $sourceDatabase.Collation 27 | PrimaryFilesize = $sourceDatabase.FileGroups['PRIMARY'].Files[0].Size / 1024 28 | } 29 | 30 | # Create the new database on the target instance. 31 | $null = New-DbaDatabase @targetDatabaseParams 32 | 33 | # Copy all the tables including the data. 34 | $transferParams = @{ 35 | SqlInstance = $sourceDatabase.Parent 36 | DestinationSqlInstance = $targetServer 37 | Database = $sourceDatabase.Name 38 | CopyAll = 'Tables' 39 | } 40 | Invoke-DbaDbTransfer @transferParams 41 | -------------------------------------------------------------------------------- /sp_Blitz_markdown/README.md: -------------------------------------------------------------------------------- 1 | # Output results of sp_Blitz in Markdown format 2 | 3 | As `sp_Blitz @OutputType = 'markdown'` excludes all the security findings, I wanted to convert the T-SQL code from inside of sp_Blitz into PowerShell and make it even more flexible. 4 | 5 | 6 | 7 | Here is the T-SQL code: 8 | https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/blob/99c3073b26d255fde928ef0e3076e7600e687596/sp_Blitz.sql#L9375-L9395 9 | 10 | 11 | 12 | And here is the code you also find in the sp_Blitz_output_as_markdown.ps1: 13 | 14 | ```powershell 15 | # Some configuration options. Just play with them and see how they work. 16 | $newLine = "`r`n" 17 | $noNewLineForOneFinding = $true 18 | $noNewLineForOneDatabase = $true 19 | 20 | # I love dbatools, so I use Invoke-DbaQuery to get the data. But Invoke-Sqlcmd would probably work as well. 21 | $sqlInstance = 'sql01' 22 | $spBlitzOutput = Invoke-DbaQuery -SqlInstance $sqlInstance -Query 'sp_Blitz @CheckServerInfo = 1' 23 | 24 | # We filter the data the same way "sp_Blitz @OutputType = 'markdown'" would do, but still include the security findings. You can adjust this if needed. 25 | $spBlitzOutput = $spBlitzOutput | Where-Object -FilterScript { $_.Priority -gt 0 -and $_.Priority -lt 255 -and $_.FindingsGroup -ne [DBNull]::Value -and $_.Finding -ne [DBNull]::Value } 26 | 27 | # Let's start with a blank output string. 28 | $output = '' 29 | 30 | # First we group by both Priority and FindingsGroup and loop through the groups. 31 | $groupsForFindingsGroup = $spBlitzOutput | Group-Object Priority, FindingsGroup 32 | foreach ($groupForFindingsGroup in $groupsForFindingsGroup) { 33 | # The name of the group contains both Priority and FindingsGroup in one string, 34 | # so we have to split and rearrange. 35 | ($priority, $findingsGroup) = $groupForFindingsGroup.Name -split ', ' 36 | $output += '**Priority ' + $priority + ': ' + $findingsGroup + '**' + $newLine 37 | 38 | # Second we group on Finding and loop through the groups. 39 | $groupsForFinding = $groupForFindingsGroup.Group | Group-Object Finding 40 | foreach ($groupForFinding in $groupsForFinding) { 41 | # We just output the Finding as an item in a bulleted list. 42 | $finding = $groupForFinding.Name 43 | $output += '- ' + $finding + $newLine 44 | 45 | # Third we group on Details and loop through the groups. 46 | # Here it is important to get an array of groups, even if we have one group, 47 | # because we need the Count as number of groups in the array 48 | # and not the number of members in the group. 49 | # So we start with an empty array and add the groups to it. 50 | # (Declaring the variable would be another way, just go for it if you like.) 51 | $groupsForDetails = @( ) 52 | $groupsForDetails += $groupForFinding.Group | Group-Object Details 53 | $groupsForDetailsCount = $groupsForDetails.Count 54 | foreach ($groupForDetails in $groupsForDetails) { 55 | $details = $groupForDetails.Name 56 | # Depending on what you have configured, if we only have one Details group, 57 | # we may just append the Details to the previous line with the Finding. 58 | # Otherwise we output the Details as a bulleted list one level below the Finding. 59 | if ($groupsForDetailsCount -eq 1 -and $noNewLineForOneFinding) { 60 | $output = $output.TrimEnd($newLine) + ': ' + $details + $newLine 61 | } else { 62 | $output += ' - ' + $details + $newLine 63 | } 64 | 65 | # Forth we group on DatabaseName and loop through the groups. 66 | # Again we need the number of groups to be able to append the previous line. 67 | $groupsForDatabaseName = @( ) 68 | $groupsForDatabaseName += $groupForDetails.Group | Group-Object DatabaseName | Where-Object Name 69 | $groupsForDatabaseNameCount = $groupsForDatabaseName.Count 70 | foreach ($groupForDatabaseName in $groupsForDatabaseName) { 71 | $databaseName = $groupForDatabaseName.Name 72 | if ($groupsForDatabaseNameCount -eq 1 -and $noNewLineForOneDatabase) { 73 | $output = $output.TrimEnd($newLine) + ' (' + $databaseName + ')' + $newLine 74 | } else { 75 | $output += ' * ' + $databaseName + $newLine 76 | } 77 | } 78 | } 79 | } 80 | # To have an empty line between the different FindingsGroups, we need three empty lines. 81 | $output += $newLine + $newLine + $newLine 82 | } 83 | # Just trim the empty lines at the end and copy the output to the clipboard. 84 | $output = $output.TrimEnd($newLine) 85 | $output | Set-Clipboard 86 | 87 | # Now just paste the clipboard into you favorite markdown editor. 88 | # If you don't have one yet, have a look at Typora or just use Notepad or Notepad++. 89 | ``` 90 | 91 | 92 | 93 | Have fun with the code. Feedback is welcome. 94 | -------------------------------------------------------------------------------- /sp_Blitz_markdown/sp_Blitz_output_as_markdown.ps1: -------------------------------------------------------------------------------- 1 | # Some configuration options. Just play with them and see how they work. 2 | $newLine = "`r`n" 3 | $noNewLineForOneFinding = $true 4 | $noNewLineForOneDatabase = $true 5 | 6 | # I love dbatools, so I use Invoke-DbaQuery to get the data. But Invoke-Sqlcmd would probably work as well. 7 | $sqlInstance = 'sql01' 8 | $spBlitzOutput = Invoke-DbaQuery -SqlInstance $sqlInstance -Query 'sp_Blitz @CheckServerInfo = 1' 9 | 10 | # We filter the data the same way "sp_Blitz @OutputType = 'markdown'" would do, but still include the security findings. You can adjust this if needed. 11 | $spBlitzOutput = $spBlitzOutput | Where-Object -FilterScript { $_.Priority -gt 0 -and $_.Priority -lt 255 -and $_.FindingsGroup -ne [DBNull]::Value -and $_.Finding -ne [DBNull]::Value } 12 | 13 | # Let's start with a blank output string. 14 | $output = '' 15 | 16 | # First we group by both Priority and FindingsGroup and loop through the groups. 17 | $groupsForFindingsGroup = $spBlitzOutput | Group-Object Priority, FindingsGroup 18 | foreach ($groupForFindingsGroup in $groupsForFindingsGroup) { 19 | # The name of the group contains both Priority and FindingsGroup in one string, 20 | # so we have to split and rearrange. 21 | ($priority, $findingsGroup) = $groupForFindingsGroup.Name -split ', ' 22 | $output += '**Priority ' + $priority + ': ' + $findingsGroup + '**' + $newLine 23 | 24 | # Second we group on Finding and loop through the groups. 25 | $groupsForFinding = $groupForFindingsGroup.Group | Group-Object Finding 26 | foreach ($groupForFinding in $groupsForFinding) { 27 | # We just output the Finding as an item in a bulleted list. 28 | $finding = $groupForFinding.Name 29 | $output += '- ' + $finding + $newLine 30 | 31 | # Third we group on Details and loop through the groups. 32 | # Here it is important to get an array of groups, even if we have one group, 33 | # because we need the Count as number of groups in the array 34 | # and not the number of members in the group. 35 | # So we start with an empty array and add the groups to it. 36 | # (Declaring the variable would be another way, just go for it if you like.) 37 | $groupsForDetails = @( ) 38 | $groupsForDetails += $groupForFinding.Group | Group-Object Details 39 | $groupsForDetailsCount = $groupsForDetails.Count 40 | foreach ($groupForDetails in $groupsForDetails) { 41 | $details = $groupForDetails.Name 42 | # Depending on what you have configured, if we only have one Details group, 43 | # we may just append the Details to the previous line with the Finding. 44 | # Otherwise we output the Details as a bulleted list one level below the Finding. 45 | if ($groupsForDetailsCount -eq 1 -and $noNewLineForOneFinding) { 46 | $output = $output.TrimEnd($newLine) + ': ' + $details + $newLine 47 | } else { 48 | $output += ' - ' + $details + $newLine 49 | } 50 | 51 | # Forth we group on DatabaseName and loop through the groups. 52 | # Again we need the number of groups to be able to append the previous line. 53 | $groupsForDatabaseName = @( ) 54 | $groupsForDatabaseName += $groupForDetails.Group | Group-Object DatabaseName | Where-Object Name 55 | $groupsForDatabaseNameCount = $groupsForDatabaseName.Count 56 | foreach ($groupForDatabaseName in $groupsForDatabaseName) { 57 | $databaseName = $groupForDatabaseName.Name 58 | if ($groupsForDatabaseNameCount -eq 1 -and $noNewLineForOneDatabase) { 59 | $output = $output.TrimEnd($newLine) + ' (' + $databaseName + ')' + $newLine 60 | } else { 61 | $output += ' * ' + $databaseName + $newLine 62 | } 63 | } 64 | } 65 | } 66 | # To have an empty line between the different FindingsGroups, we need three empty lines. 67 | $output += $newLine + $newLine + $newLine 68 | } 69 | # Just trim the empty lines at the end and copy the output to the clipboard. 70 | $output = $output.TrimEnd($newLine) 71 | $output | Set-Clipboard 72 | 73 | # Now just paste the clipboard into you favorite markdown editor. 74 | # If you don't have one yet, have a look at Typora or just use Notepad or Notepad++. 75 | --------------------------------------------------------------------------------