diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/Databases/GlobalDataDB.bak b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/Databases/GlobalDataDB.bak new file mode 100644 index 000000000..eba6d2f27 Binary files /dev/null and b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/Databases/GlobalDataDB.bak differ diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/Databases/LocalMasterDataDB.bak b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/Databases/LocalMasterDataDB.bak new file mode 100644 index 000000000..17a0f9f56 Binary files /dev/null and b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/Databases/LocalMasterDataDB.bak differ diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/Databases/SharedMasterDataDB.bak b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/Databases/SharedMasterDataDB.bak new file mode 100644 index 000000000..934d06505 Binary files /dev/null and b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/Databases/SharedMasterDataDB.bak differ diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/Databases/TenantCRM.bak b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/Databases/TenantCRM.bak new file mode 100644 index 000000000..77216cbfc Binary files /dev/null and b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/Databases/TenantCRM.bak differ diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/Databases/TenantDataDB.bak b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/Databases/TenantDataDB.bak new file mode 100644 index 000000000..254e460f6 Binary files /dev/null and b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/Databases/TenantDataDB.bak differ diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/Docs/Hack Environment - Setup and Reset MASTER.docx b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/Docs/Hack Environment - Setup and Reset MASTER.docx new file mode 100644 index 000000000..3821632da Binary files /dev/null and b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/Docs/Hack Environment - Setup and Reset MASTER.docx differ diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/01-Data_Migration/Check_ManagedInstanceLink_ReplicaStatus.sql b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/01-Data_Migration/Check_ManagedInstanceLink_ReplicaStatus.sql new file mode 100644 index 000000000..b31eadd29 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/01-Data_Migration/Check_ManagedInstanceLink_ReplicaStatus.sql @@ -0,0 +1,11 @@ +SELECT + DB_NAME(drs.database_id) AS DatabaseName, + drs.synchronization_state_desc, + drs.synchronization_health_desc, + drs.is_commit_participant, + drs.log_send_queue_size, + drs.redo_queue_size, + drs.last_commit_time +FROM sys.dm_hadr_database_replica_states drs +WHERE DB_NAME(drs.database_id) LIKE 'TEAM01%' +ORDER BY DatabaseName; \ No newline at end of file diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/02-Administering_Monitoring/Part_01_Monitoring_Lab_1.sql b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/02-Administering_Monitoring/Part_01_Monitoring_Lab_1.sql new file mode 100644 index 000000000..e87f61b39 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/02-Administering_Monitoring/Part_01_Monitoring_Lab_1.sql @@ -0,0 +1,35 @@ +/************************************************************************************************************************************ + * + * ================ + * MONITORING LAB + * ================ + + * The databases are hosted on: + * + * sqlhackmiXXXXX.database.windows.net + * + ************************************************************************************************************************************/ + + + + +/************************************************************************************************************************************ + * PART 1: USING [sys].[dm_db_resource_stats] DMV TO LOOK AT THE SQL MI RESOURCES STATS + ************************************************************************************************************************************/ + +--QUERY 1 +SELECT * FROM sys.dm_db_resource_stats ; -- measure each 15 seconds over 1h +/* + * Note the [avg_cpu_percent] is above 80% indicating that the CPU could be under pressure. + */ +--QUERY 2 +SELECT end_time,avg_cpu_percent,avg_data_io_percent FROM sys.dm_db_resource_stats order by end_time asc; + +-- QUERY 3 +-- For a wide historic up to 14 days +select start_time,avg_cpu_percent,io_requests,* from sys.server_resource_stats + +/* + * END PART 1 - RETURN TO LAB INSTRUCTIONS + */ + diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/02-Administering_Monitoring/Part_02_Monitoring_Lab_1.sql b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/02-Administering_Monitoring/Part_02_Monitoring_Lab_1.sql new file mode 100644 index 000000000..9b6ae8448 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/02-Administering_Monitoring/Part_02_Monitoring_Lab_1.sql @@ -0,0 +1,55 @@ + /* + * ================ + * MONITORING LAB + * ================ + + * The databases are hosted on: + * + * sqlhackmiXXXXX.database.windows.net + * + ************************************************************************************************************************************/ + + +/************************************************************************************************************************************ + * PART 2: USING [sys].[dm_exec_procedure_stats] TO EXAMINE EXECUTION STATS + ************************************************************************************************************************************/ + + --This query uses the [sys].[dm_exec_procedure_stats] DMV identify most consuming stored procedures and the details + -- QUERY 1 +select OBJECT_NAME(object_id,database_id) as ProcedureName + ,total_worker_time/1000 as total_worker_time_ms,min_worker_time/1000 as min_worker_time,max_worker_time/1000 as max_worker_time_ms,(total_worker_time/execution_count)/1000 as avg_worker_time_ms + ,total_elapsed_time/1000 as total_elapsed_time_ms,min_elapsed_time/1000 as min_elapsed_time_ms,max_elapsed_time/1000 as max_elapsed_time_ms,(total_elapsed_time/execution_count)/1000 as avg_execution_time_ms + ,total_logical_reads,min_logical_reads,max_logical_reads,(total_logical_reads/execution_count) as avg_logical_reads + ,total_physical_reads,min_physical_reads,max_physical_reads,(total_physical_reads/execution_count) as avg_physical_reads + ,total_logical_writes,min_logical_writes,max_logical_writes,(total_logical_writes/execution_count) as avg_logical_writes,type,cached_time,last_execution_time,execution_count +from sys.dm_exec_procedure_stats s +Where OBJECT_NAME(object_id,database_id) is not NULL +order by s.total_worker_time desc + +-- QUERY 2 replace the filter clause with the SP identify +-- Procedure statement details +select OBJECT_NAME(object_id,database_id) as ProcedureName,st.text +,SUBSTRING(st.text, (qs.statement_start_offset/2)+1, +((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(st.text) +ELSE qs.statement_end_offset +END - qs.statement_start_offset)/2) + 1) AS statement_text +,qp.query_plan +,qs.execution_count +,qs.total_worker_time,qs.min_worker_time,qs.max_worker_time,(qs.total_worker_time/qs.execution_count) as avg_worker_time +,qs.total_elapsed_time,qs.min_elapsed_time,qs.max_elapsed_time,(qs.total_elapsed_time/qs.execution_count) as avg_execution_time +,qs.total_logical_reads,qs.min_logical_reads,qs.max_logical_reads,(qs.total_logical_reads/qs.execution_count) as avg_logical_reads +,qs.total_physical_reads,qs.min_physical_reads,qs.max_physical_reads,(qs.total_physical_reads/qs.execution_count) as avg_physical_reads +,qs.total_logical_writes,qs.min_logical_writes,qs.max_logical_writes,(qs.total_logical_writes/qs.execution_count) as avg_logical_writes + +,* + from sys.dm_exec_procedure_stats ps + left join sys.dm_exec_query_stats qs + on ps.sql_handle=qs.sql_handle + cross apply sys.dm_exec_sql_text(qs.sql_handle) st + cross apply sys.dm_exec_query_plan(qs.plan_handle) qp +where OBJECT_NAME(object_id,database_id) ='MyProc' + order by ProcedureName,qs.total_worker_time desc +/* + * END PART 2 - RETURN TO LAB INSTRUCTIONS + */ + diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/03-Security/ATP_Demo.sql b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/03-Security/ATP_Demo.sql new file mode 100644 index 000000000..d2e0a5966 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/03-Security/ATP_Demo.sql @@ -0,0 +1,4 @@ +--Advanced Threat Protection +DECLARE @UserInput nvarchar(max) = ''' OR 1=1--'; +DECLARE @SQL nvarchar(max) = 'SELECT * FROM sys.databases WHERE name = ''' + @UserInput + ''''; +EXEC(@SQL); diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/03-Security/SQLScripts/2. Auditing.sql b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/03-Security/SQLScripts/2. Auditing.sql new file mode 100644 index 000000000..7baa89590 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/03-Security/SQLScripts/2. Auditing.sql @@ -0,0 +1,73 @@ +--- **** REPLACE TEAMXX with your TEAM number e.g TEAM01 **---- + +--1. Create an audit for your Managed Instance and define the target +-- Create the server audit. +-- Change the path to a path to Blob Storage Path for auditing +USE MASTER +GO +CREATE SERVER AUDIT [TEAMXX_server_audit] +TO URL(PATH ='https://sqlhacksapfu2ietzqrxem.blob.core.windows.net/auditlogs') +GO + + +--2. Create a database audit specification that maps to the audit by running the following query in Management Studio + +USE [TEAMXX_TenantDataDb] +GO +-- Create the database audit specification. +CREATE DATABASE AUDIT SPECIFICATION TEAMXX_Audit_Data --change xx to team number +FOR SERVER AUDIT [TEAMXX_server_audit] --change to server audit name created in step 1 +ADD (INSERT, UPDATE, DELETE, SELECT + ON UserTransactions BY dbo ) --[UserTransactions] is the target table we're going to audit +WITH (STATE = ON); +GO + +/** +--- You will need to configure blob storage for audit logs using the SQL Server Management Studio (SSMS) 18 (Preview): +-- 1. Connect to the managed instance using SQL Server Management Studio (SSMS) UI +-- 2. Expand the Security node, right-click on the Audits node, and click on "TEAMXX_server_audit" +-- 3. Make sure "URL" is selected in Audit destination and click on Browse: +-- 4. Sign in to your Azure account +-- 5. Select your subscription, sqlhacksapfu2ietzqrxem, and sqlaudits from the dropdowns. Once you have finished click OK. +**/ + + +-- enable the audit +-- ensure you have connected to the audit log file location + +USE MASTER +GO +ALTER SERVER AUDIT [TEAMXX_server_audit] WITH (STATE = ON) + +--3. Run a couple of SELECT and DELETE Statements on TEAMXX_TenantDataDb which you have enabled. + +-- Run select statements and delete statments below: +USE [TEAMXX_TenantDataDb] +GO +SELECT * FROM TEAMXX_TenantDataDb.dbo.usertransactions +WHERE UserTranID < 1000 + +SELECT * FROM TEAMXX_TenantDataDb.dbo.usertransactions +WHERE UserTranID > 1000 + +-- delete transaction +DELETE FROM TEAMXX_TenantDataDb.dbo.usertransactions +WHERE UserTranID = 1000 + + +--4. Read the audit events by using the fn_get_audit_file function. The directory hierarchy within the container is of the form ////. + +-- check the audit logs +SELECT event_time, +action_id, +client_ip, +session_server_principal_name, +database_principal_name, +statement, +application_name, +data_sensitivity_information, --new column added to audit log +host_name +FROM sys.fn_get_audit_file ('https://sqlhacksapfu2ietzqrxem.blob.core.windows.net/auditlogs/sqlhackmi-pfu2ietzqrxem/master/', default, default) -- change the location to reflect the directory of the XEL file in your blob storage +WHERE data_sensitivity_information like '%confidential%' +AND database_name = 'TEAMXX_TenantDataDb' +GO-- change the location to reflect the directory of the XEL file in your blob storage diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/03-Security/SQLScripts/3. Dynamic Data Masking.sql b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/03-Security/SQLScripts/3. Dynamic Data Masking.sql new file mode 100644 index 000000000..b3b8868f7 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/03-Security/SQLScripts/3. Dynamic Data Masking.sql @@ -0,0 +1,42 @@ +-- *** REPLACE XX WITH YOUR TEAM NUMBER ***--- + +--1. Run the below script to mask the transaction amount in the UserTransactions table using the default masking function +USE TEAMXX_TenantDataDB; +GO +ALTER TABLE [UserTransactions] +ALTER COLUMN [TranAmount] [decimal](18, 2) MASKED WITH (FUNCTION = 'DEFAULT()') + + +--3. Check to see masked columns +SELECT c.name, tbl.name as table_name, c.is_masked, c.masking_function +FROM sys.masked_columns AS c +JOIN sys.tables AS tbl + ON c.[object_id] = tbl.[object_id] +WHERE is_masked = 1; + + +--4. Check the table to ensure you are still able to see ALL the data +SELECT * FROM TEAMXX_TenantDataDB.dbo.UserTransactions +GO + +--5. Create a user for the developer and grant the read only access on the UserTransactions table +CREATE USER Peter WITHOUT LOGIN +GRANT SELECT ON dbo.UserTransactions TO Peter; + +--6. Run the query below to ensure we have been able to prevent the developer from accessing customers’ privacy information. This will run the SELECT statement as the new user, Peter, which was created in the previous step +EXECUTE AS USER = 'Peter'; +SELECT * FROM TEAMXX_TenantDataDb.dbo.UserTransactions; +REVERT; + +--7. Grant Peter permission to Peter to see unmasked data +GRANT UNMASK TO Peter; +EXECUTE AS USER = 'Peter'; +SELECT * FROM TEAMXX_TenantDataDb.dbo.UserTransactions; +REVERT; + +--8. Clean up masked column +USE TEAMXX_TenantDataDB; +GO +ALTER TABLE [UserTransactions] +ALTER COLUMN [TranAmount] DROP MASKED; + diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/03-Security/SQLScripts/4. TDE and Password Reset.sql b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/03-Security/SQLScripts/4. TDE and Password Reset.sql new file mode 100644 index 000000000..cf18211a5 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/03-Security/SQLScripts/4. TDE and Password Reset.sql @@ -0,0 +1,16 @@ +--Enable TDE for databases +USE [master] +GO +ALTER DATABASE [TEAMXX_LocalMasterDataDB] SET ENCRYPTION ON +GO + +ALTER DATABASE [TEAMXX_SharedMasterDataDB] SET ENCRYPTION ON +GO + +ALTER DATABASE [TEAMXX_TenantDataDB] SET ENCRYPTION ON +GO + +-- ALTER LOGIN [$0] WITH PASSWORD = '' +ALTER LOGIN [TEAMXX] DISABLE; +-- DBA must change the password before re-enable. +ALTER LOGIN [teamXX] WITH PASSWORD = '' \ No newline at end of file diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/Readme.pdf b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/Readme.pdf new file mode 100644 index 000000000..b983ccb48 Binary files /dev/null and b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/Readme.pdf differ diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/challenge-01.pdf b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/challenge-01.pdf new file mode 100644 index 000000000..fbd5eb910 Binary files /dev/null and b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/challenge-01.pdf differ diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/challenge-02.pdf b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/challenge-02.pdf new file mode 100644 index 000000000..12b9e3a47 Binary files /dev/null and b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/challenge-02.pdf differ diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/challenge-03.pdf b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/challenge-03.pdf new file mode 100644 index 000000000..1d4c73fc3 Binary files /dev/null and b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/challenge-03.pdf differ diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/solution-01.pdf b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/solution-01.pdf new file mode 100644 index 000000000..7335e41dd Binary files /dev/null and b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/solution-01.pdf differ diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/solution-02.pdf b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/solution-02.pdf new file mode 100644 index 000000000..8568d6bed Binary files /dev/null and b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/solution-02.pdf differ diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/solution-03.pdf b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/solution-03.pdf new file mode 100644 index 000000000..918aa0906 Binary files /dev/null and b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/LABS/solution-03.pdf differ diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/TSQL_Scripts/configure_audit.sql b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/TSQL_Scripts/configure_audit.sql new file mode 100644 index 000000000..d16ae53ee --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/TSQL_Scripts/configure_audit.sql @@ -0,0 +1,17 @@ +-- Managed Identity must be enabled on the SQL Managed Instance for this to work +-- Configure the Managed Identity for the SQL Managed Instance to have access to the storage account (Storage Blob Data Contributor role) and the storage account must have a container named "auditlogs" created in it. +-- Please replace with the name of your storage account in the below script. + +USE master; +CREATE CREDENTIAL [https://.blob.core.windows.net/auditlogs] +WITH IDENTITY='MANAGED IDENTITY' +GO + +CREATE SERVER AUDIT [sqlmi_auditlog] +TO URL ( PATH ='https://.blob.core.windows.net/auditlogs' +, RETENTION_DAYS = 30 ) +GO + +ALTER SERVER AUDIT [sqlmi_auditlog] +WITH (STATE = ON) +GO diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/assets/BaseWallpaper.jpg b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/assets/BaseWallpaper.jpg new file mode 100644 index 000000000..0b44529f4 Binary files /dev/null and b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/assets/BaseWallpaper.jpg differ diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/deploy-lab.ps1 b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/deploy-lab.ps1 new file mode 100644 index 000000000..a4f992441 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/deploy-lab.ps1 @@ -0,0 +1,128 @@ +<# +.SYNOPSIS +Deploys the lab resources scoped to a subscription or resource group. +.DESCRIPTION +Provides a controlled deployment flow for lab environments, optionally limited to a resource group and specific Entra user IDs. +.PARAMETER DeploymentType +Defines the deployment scope; allowed values are subscription or resourcegroup. +.PARAMETER SubscriptionId +Specifies the Azure subscription that contains the lab resources. +.PARAMETER ResourceGroupName +In case of resourcegroup deployment, specifies the target resource group name. +.PARAMETER PreferredLocation +Specifies the preferred Azure regions (ordered by preference) for resource deployment. An empty array indicates no preference. +.PARAMETER AllowedEntraUserIds +Optional list of Entra user object IDs permitted to access the lab resources. +#> +param( + [Parameter(Mandatory=$true)] + [ValidateSet('subscription','resourcegroup', 'resourcegroup-with-subscriptionowner')] + [string]$DeploymentType, + + [Parameter(Mandatory=$true)] + [string]$SubscriptionId, + + [string]$ResourceGroupName = "", + + [string[]]$PreferredLocation = @(), + + [string[]]$AllowedEntraUserIds = @() +) + +$ErrorActionPreference = 'Stop' + +$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +Write-Host "[$SubscriptionId] Running deploy-lab.ps1 in $scriptPath" + +# Lab environment configuration +$environmentName = "sqlhack" +$sharedResourceGroupName = "rg-sqlhack-shared" +$adminUsername = "DemoUser" +$adminPassword = New-MhhStablePassword -Purpose 'vm-admin' -SubscriptionId $SubscriptionId -ResourceGroupName "Default" + +# Template file path +$templatePath = Join-Path $scriptPath "infra" +$template = Join-Path $templatePath "main.bicep" +Write-Host "[$SubscriptionId] Deploying lab resources from template $template" + +#if ($AllowedEntraUserIds.Count -eq 0) { +# throw "No Entra user object ID was provided. The MicroHack platform must pass at least one value in AllowedEntraUserIds." +#} + +# set the effective location (example) +if($PreferredLocation.Count -gt 0) { + $effectiveLocation = $PreferredLocation[0] +} else { + $effectiveLocation = "swedencentral" # Default location if no preference is provided +} +$deploymentLocations = if ($PreferredLocation.Count -gt 0) { $PreferredLocation } else { @($effectiveLocation) } + +# set the effective resource group based on deployment type (example) +if($DeploymentType -eq 'subscription') { + $stableHash = Get-MhhStableHash -Value $AllowedEntraUserIds -Length 24 + $effectiveResourceGroup = "lab-$stableHash" + Write-Host "Deploying lab resources at the subscription level in subscription $SubscriptionId..." + if(-not (Get-AzResourceGroup -Name $effectiveResourceGroup -ErrorAction SilentlyContinue)) { + Write-Host "Creating resource group '$effectiveResourceGroup' in location '$effectiveLocation'..." + New-AzResourceGroup -Name $effectiveResourceGroup -Location $effectiveLocation -Verbose + } else { + Write-Host "Resource group '$effectiveResourceGroup' already exists." + } +} +else { + if ([string]::IsNullOrWhiteSpace($ResourceGroupName)) { + throw "ResourceGroupName is empty for deployment type '$DeploymentType'. The MicroHack platform must provide the participant resource group." + } + $effectiveResourceGroup = $ResourceGroupName +} + +Write-Host "[$SubscriptionId] Deploying to user: $($AllowedEntraUserIds[0])" + +$me = Get-MhhLabUser -UserId @($AllowedEntraUserIds)[0] +Write-Host "Deploying for $($me.UserPrincipalName) ($($me.ShortName))" +$vmPostfix = "$(($me.ShortName -split "\.")[0])" +if ($vmPostfix.Length -gt 12) { + $vmPostfix = $vmPostfix.Substring(0,12) +} +$vmPostfix = $vmPostfix -replace "_", "-" +$vmName = "VM-$vmPostfix" +$TeamName = $me.ShortName +$TeamName = $TeamName.ToUpper() +$legacySQLName = "legacySQL2016" + + +$storageAccountName = Get-AzStorageAccount -ResourceGroupName $sharedResourceGroupName -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty StorageAccountName +$managedInstanceFQDN = Get-AzSqlInstance -ResourceGroupName $sharedResourceGroupName -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty FullyQualifiedDomainName + +Write-Host "[$SubscriptionId] Using storage account: $storageAccountName" +Write-Host "[$SubscriptionId] Using managed instance FQDN: $managedInstanceFQDN" +Write-Host "[$SubscriptionId] Using Computer Name: $vmName" + +# Deploy lab resources using Invoke-MhhDeploymentWithRegionFallback +$result = Invoke-MhhDeploymentWithRegionFallback ` + -PreferredLocations $deploymentLocations ` + -ResourceGroupName $effectiveResourceGroup ` + -RgOwnerEntraObjectIds $AllowedEntraUserIds ` + -TemplateFile $template ` + -TemplateParameterObject @{ + environmentName = $environmentName + location = $effectiveLocation + sharedResourceGroupName = $sharedResourceGroupName + adminUsername = $adminUsername + adminPassword = $adminPassword + storageAccountName = $storageAccountName + managedInstanceFQDN = $managedInstanceFQDN + vmName = $vmName + TeamName = $TeamName + legacySQLName = $legacySQLName + } + +Write-Host "[$SubscriptionId] Lab deployment completed successfully" +Write-Host "[$SubscriptionId] Lab Resource Group: $effectiveResourceGroup" +Write-Host "[$SubscriptionId] Shared Resource Group: $sharedResourceGroupName" +Write-Host "[$SubscriptionId] Result: $($result)" + +# feed the effective resource group back to the console +@{"HackboxCredential" = @{ name = "ResourceGroupName" ; value = $effectiveResourceGroup; note = "The name of the resource group where lab resources are deployed" }} +@{"HackboxCredential" = @{ name = "Team VM Name" ; value = $vmName; note = "The name of the Team VM" }} +@{"HackboxCredential" = @{ name = "Team / Link Name" ; value = $TeamName; note = "Name of the Team (Prefix for DBs and to use as Link Name for SQLMI)" }} diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/main-shared.bicep b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/main-shared.bicep new file mode 100644 index 000000000..e670249bf --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/main-shared.bicep @@ -0,0 +1,211 @@ +targetScope = 'subscription' + +@minLength(1) +@maxLength(20) +param environmentName string + +param location string + +param repoBaseURL string = 'https://raw.githubusercontent.com/microsoft/MicroHack/main/03-Azure/01-02%20Data/02-SQL_Modernization/labautomation' + +param adminUsername string +@secure() +param adminPassword string + +param sqlMiAdminUsername string +@secure() +param sqlMiAdminPassword string + +param vnetAddressPrefix string = '10.0.0.0/16' +param managedInstanceSubnetPrefix string = '10.0.1.0/24' +param managementSubnetPrefix string = '10.0.2.0/24' +param teamSubnetPrefix string = '10.0.3.0/24' +param bastionSubnetPrefix string = '10.0.4.0/24' +param PrivateEndpointsSubnetPrefix string = '10.0.5.0/24' + +param legacySQLName string +param arcSQLName string + +param legacyVmSize string = 'Standard_D4s_v5' +param sqlarcVmSize string = 'Standard_D2s_v5' + +param managedInstanceVCores int = 8 +param managedInstanceStorageGB int = 256 + +param tags object = { + workload: 'SQL Modernization MicroHack' + environment: environmentName + managedBy: 'azd-bicep' +} + +var suffix = uniqueString( + subscription().subscriptionId, + environmentName, + location +) + +var resourceGroupName = 'rg-${environmentName}-shared' +var vnetName = 'SQLHACK-SHARED-VNET' +var managedInstanceName = 'sqlmi-${environmentName}-${suffix}' + +var storageAccountName = take( + toLower( + replace( + 'sqlhack${environmentName}${suffix}', + '-', + '' + ) + ), + 24 +) + +resource resourceGroup 'Microsoft.Resources/resourceGroups@2024-11-01' existing = { + name: resourceGroupName +} + +module network 'modules/network.bicep' = { + name: 'network' + scope: resourceGroup + params: { + location: location + vnetName: vnetName + vnetAddressPrefix: vnetAddressPrefix + managedInstanceSubnetPrefix: managedInstanceSubnetPrefix + managementSubnetPrefix: managementSubnetPrefix + teamSubnetPrefix: teamSubnetPrefix + bastionSubnetPrefix: bastionSubnetPrefix + PrivateEndpointsSubnetPrefix: PrivateEndpointsSubnetPrefix + tags: tags + } +} + +module bastion 'modules/bastion.bicep' = { + name: 'bastion' + scope: resourceGroup + params: { + location: location + bastionName: 'bas-${environmentName}' + bastionSubnetId: network.outputs.bastionSubnetId + tags: tags + } +} + +module legacySqlVm 'modules/sql2016-vm.bicep' = { + name: '${legacySQLName}-vm' + scope: resourceGroup + params: { + location: location + vmName: legacySQLName + subnetId: network.outputs.managementSubnetId + privateIPAddress: '10.0.2.5' + vmSize: legacyVmSize + adminUsername: adminUsername + adminPassword: adminPassword + tags: tags + } +} + +module sqlarcSqlVm 'modules/sql2022-vm.bicep' = { + name: '${arcSQLName}-vm' + scope: resourceGroup + params: { + location: location + vmName: arcSQLName + subnetId: network.outputs.managementSubnetId + privateIPAddress: '10.0.2.6' + vmSize: sqlarcVmSize + adminUsername: adminUsername + adminPassword: adminPassword + tags: tags + } +} + +module managedInstance 'modules/managed-instance.bicep' = { + name: 'managed-instance' + scope: resourceGroup + params: { + location: location + managedInstanceName: managedInstanceName + subnetId: network.outputs.managedInstanceSubnetId + privateEndpointName: '${managedInstanceName}-pe' + privateEndpointSubnetId: network.outputs.PrivateEndpointsSubnetId + vnetId: network.outputs.vnetId + administratorLogin: sqlMiAdminUsername + administratorLoginPassword: sqlMiAdminPassword + vCores: managedInstanceVCores + storageSizeInGB: managedInstanceStorageGB + tags: tags + } +} + +module storage 'modules/storage.bicep' = { + name: 'storage' + scope: resourceGroup + params: { + location: location + storageAccountName: storageAccountName + privateEndpointName: '${storageAccountName}-pe' + privateEndpointSubnetId: network.outputs.PrivateEndpointsSubnetId + vnetId: network.outputs.vnetId + tags: tags + } +} + +module storage_rbac 'modules/storage-rbac.bicep' = { + name: 'storage_rbac' + //dependsOn: [ + // storage + // legacySqlVm + // managedInstance + //] + scope: resourceGroup + params: { + storageAccountName: storage.outputs.storageAccountName + vmPrincipalId: legacySqlVm.outputs.vmPrincipalId + sqlmiPrincipalId: managedInstance.outputs.sqlmiPrincipalId + } +} + +module legacySqlVm_cse 'modules/sql2016-vm-cse.bicep' = { + name: 'legacy-sql-vm_cse' + scope: resourceGroup + params: { + location: location + vmName: legacySQLName + adminUsername: adminUsername + adminPassword: adminPassword + sqlMiAdminUsername: sqlMiAdminUsername + sqlMiAdminPassword: sqlMiAdminPassword + storageAccountName: storageAccountName + managedInstanceServer: managedInstance.outputs.fullyQualifiedDomainName + repoBaseURL: repoBaseURL + } +} + +module sqlarcSqlVm_cse 'modules/sql2022-vm-cse.bicep' = { + name: 'sqlarc-sql-vm_cse' + scope: resourceGroup + dependsOn: [ + sqlarcSqlVm + ] + params: { + location: location + vmName: 'arcSQL2022' + adminUsername: adminUsername + adminPassword: adminPassword + repoBaseURL: repoBaseURL + } +} + +output AZURE_RESOURCE_GROUP string = resourceGroup.name +output AZURE_LOCATION string = location + +output AZURE_STORAGE_ACCOUNT_NAME string = storage.outputs.storageAccountName + +output SQL_MI_NAME string = managedInstance.outputs.managedInstanceName + +output SQL_MI_FQDN string = managedInstance.outputs.fullyQualifiedDomainName + +output LEGACY_SQL_VM_NAME string = legacySqlVm.outputs.vmName + +output BASTION_NAME string = bastion.outputs.bastionName diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/main.bicep b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/main.bicep new file mode 100644 index 000000000..14626fae2 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/main.bicep @@ -0,0 +1,86 @@ +targetScope = 'resourceGroup' + +param environmentName string + +param location string + +param vmName string +param TeamName string = 'TEAM01' +param legacySQLName string + +@description('Name of the shared resource group for accessing shared resources') +param sharedResourceGroupName string = '' + +param repoBaseURL string = 'https://raw.githubusercontent.com/microsoft/MicroHack/main/03-Azure/01-02%20Data/02-SQL_Modernization/labautomation' + +param storageAccountName string +//param managedInstanceName string +param managedInstanceFQDN string + +param adminUsername string +@secure() +param adminPassword string + +param teamVmSize string = 'Standard_D2s_v5' + +param tags object = { + workload: 'SQL Modernization MicroHack' + environment: environmentName + managedBy: 'azd-bicep' +} + +//var resourceGroupName = 'rg-${environmentName}' +var vnetName = 'SQLHACK-SHARED-VNET' + +resource vnet 'Microsoft.Network/virtualNetworks@2024-05-01' existing = { + name: vnetName + scope: resourceGroup(sharedResourceGroupName) +} + +module teamVms 'modules/team-vm.bicep' = { + name: 'team-vms' + //scope: resourceGroup + params: { + location: location + subnetId: vnet.properties.subnets[2].id + vmSize: teamVmSize + vmName: vmName + adminUsername: adminUsername + adminPassword: adminPassword + tags: tags + } +} + +module teamVms_cse 'modules/team-vm-cse.bicep' = { + name: 'team-vms_cse' + //scope: resourceGroup + dependsOn: [ + teamVms + ] + params: { + location: location + vmName: vmName + repoBaseURL: repoBaseURL + managedInstanceServer: managedInstanceFQDN + storageAccountName: storageAccountName + TeamName: TeamName + legacySQLName: legacySQLName + adminUsername: adminUsername + adminPassword: adminPassword + } +} + +//output AZURE_RESOURCE_GROUP string = resourceGroup.name +output AZURE_LOCATION string = location + +//output AZURE_STORAGE_ACCOUNT_NAME string = storage.outputs.storageAccountName + +//output SQL_MI_NAME string = managedInstance.outputs.managedInstanceName + +//output SQL_MI_FQDN string = managedInstance.outputs.fullyQualifiedDomainName + +//output LEGACY_SQL_VM_NAME string = legacySqlVm.outputs.vmName + +//output TEAM_VM_NAMES array = teamVms.outputs.vmNames + +//output BASTION_NAME string = bastion.outputs.bastionName diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/bastion.bicep b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/bastion.bicep new file mode 100644 index 000000000..f401d9509 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/bastion.bicep @@ -0,0 +1,60 @@ +param location string +param bastionName string +param bastionSubnetId string +param tags object + +resource publicIp 'Microsoft.Network/publicIPAddresses@2024-05-01' = { + name: '${bastionName}-pip' + location: location + tags: tags + + sku: { + name: 'Standard' + } + + properties: { + publicIPAllocationMethod: 'Static' + publicIPAddressVersion: 'IPv4' + } +} + +resource bastion 'Microsoft.Network/bastionHosts@2024-05-01' = { + name: bastionName + location: location + tags: tags + + sku: { + name: 'Basic' + } + + // sku: { + // name: 'Standard' + // } + + properties: { + // Enables Native Client connections (SSH/RDP via Azure CLI) + //enableTunneling: true + enableTunneling: false + // Enables the creation of browser-accessible Shareable Links + //enableShareableLink: true + enableShareableLink: false + + ipConfigurations: [ + { + name: 'bastion-ip-config' + properties: { + subnet: { + id: bastionSubnetId + } + + publicIPAddress: { + id: publicIp.id + } + } + } + ] + } +} + +output bastionName string = bastion.name +output bastionId string = bastion.id diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/managed-instance.bicep b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/managed-instance.bicep new file mode 100644 index 000000000..04b66f321 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/managed-instance.bicep @@ -0,0 +1,132 @@ +param location string +param managedInstanceName string +param subnetId string +param privateEndpointName string +param privateEndpointSubnetId string +param vnetId string +param administratorLogin string + +@secure() +param administratorLoginPassword string + +@allowed([ + 4 + 8 + 16 + 24 + 32 + 40 + 64 + 80 +]) +param vCores int = 8 + +@minValue(32) +@maxValue(16384) +param storageSizeInGB int = 256 + +param tags object + +resource managedInstance 'Microsoft.Sql/managedInstances@2025-02-01-preview' = { + name: managedInstanceName + location: location + tags: tags + + identity: { + type: 'SystemAssigned' + } + + sku: { + name: 'GP_Gen5' + tier: 'GeneralPurpose' + family: 'Gen5' + capacity: vCores + } + + properties: { + administratorLogin: administratorLogin + administratorLoginPassword: administratorLoginPassword + + subnetId: subnetId + licenseType: 'LicenseIncluded' + + vCores: vCores + storageSizeInGB: storageSizeInGB + + isGeneralPurposeV2: true + + publicDataEndpointEnabled: false + //publicDataEndpointEnabled: true + proxyOverride: 'Proxy' + //proxyOverride: 'Redirect' + minimalTlsVersion: '1.2' + requestedBackupStorageRedundancy: 'Local' + timezoneId: 'UTC' + } +} + +resource managedInstancePrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' = { + name: privateEndpointName + location: location + + properties: { + subnet: { + id: privateEndpointSubnetId + } + + privateLinkServiceConnections: [ + { + name: 'managedInstanceConnection' + + properties: { + privateLinkServiceId: managedInstance.id + groupIds: [ + 'managedInstance' + ] + } + } + ] + } +} + +resource sqlPrivateDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' = { + name: 'privatelink${environment().suffixes.sqlServerHostname}' + location: 'global' +} + +resource sqlPrivateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = { + name: 'vnet-link' + parent: sqlPrivateDnsZone + location: 'global' + + properties: { + registrationEnabled: false + + virtualNetwork: { + id: vnetId + } + } +} + +resource managedInstancePrivateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = { + name: 'default' + parent: managedInstancePrivateEndpoint + + properties: { + privateDnsZoneConfigs: [ + { + name: 'sql' + + properties: { + privateDnsZoneId: sqlPrivateDnsZone.id + } + } + ] + } +} + +output managedInstanceName string = managedInstance.name + +output fullyQualifiedDomainName string = managedInstance.properties.fullyQualifiedDomainName + +output sqlmiPrincipalId string = managedInstance.identity.principalId diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/network.bicep b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/network.bicep new file mode 100644 index 000000000..8036f6789 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/network.bicep @@ -0,0 +1,223 @@ +param location string +param vnetName string +param vnetAddressPrefix string +param managedInstanceSubnetPrefix string +param managementSubnetPrefix string +param teamSubnetPrefix string +param bastionSubnetPrefix string +param PrivateEndpointsSubnetPrefix string +param tags object + +resource managedInstanceNsg 'Microsoft.Network/networkSecurityGroups@2024-05-01' = { + name: '${vnetName}-mi-nsg' + location: location + tags: tags + + properties: { + securityRules: [ + { + name: 'allow-mi-subnet-inbound' + properties: { + priority: 100 + direction: 'Inbound' + access: 'Allow' + protocol: '*' + sourcePortRange: '*' + destinationPortRange: '*' + sourceAddressPrefix: managedInstanceSubnetPrefix + destinationAddressPrefix: '*' + } + } + { + name: 'allow-health-probe-inbound' + properties: { + priority: 110 + direction: 'Inbound' + access: 'Allow' + protocol: '*' + sourcePortRange: '*' + destinationPortRange: '*' + sourceAddressPrefix: 'AzureLoadBalancer' + destinationAddressPrefix: '*' + } + } + { + name: 'allow-tds-inbound' + properties: { + priority: 1000 + direction: 'Inbound' + access: 'Allow' + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '1433' + sourceAddressPrefix: 'VirtualNetwork' + destinationAddressPrefix: '*' + } + } + { + name: 'allow-redirect-inbound' + properties: { + priority: 1010 + direction: 'Inbound' + access: 'Allow' + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '11000-11999' + sourceAddressPrefix: 'VirtualNetwork' + destinationAddressPrefix: '*' + } + } + ] + } +} + +resource managedInstanceRouteTable 'Microsoft.Network/routeTables@2024-05-01' = { + name: '${vnetName}-mi-rt' + location: location + tags: tags + + properties: { + disableBgpRoutePropagation: false + } +} + +resource vmNsg 'Microsoft.Network/networkSecurityGroups@2024-05-01' = { + name: '${vnetName}-vm-nsg' + location: location + tags: tags + + properties: { + securityRules: [ + { + name: 'allow-rdp-from-bastion' + properties: { + priority: 100 + direction: 'Inbound' + access: 'Allow' + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '3389' + sourceAddressPrefix: bastionSubnetPrefix + destinationAddressPrefix: '*' + } + } + { + name: 'allow-sql-from-vnet' + properties: { + priority: 110 + direction: 'Inbound' + access: 'Allow' + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '1433' + sourceAddressPrefix: 'VirtualNetwork' + destinationAddressPrefix: '*' + } + } + ] + } +} + +resource vnet 'Microsoft.Network/virtualNetworks@2024-05-01' = { + name: vnetName + location: location + tags: tags + + properties: { + addressSpace: { + addressPrefixes: [ + vnetAddressPrefix + ] + } + + subnets: [ + { + name: 'ManagedInstance' + properties: { + addressPrefix: managedInstanceSubnetPrefix + + networkSecurityGroup: { + id: managedInstanceNsg.id + } + + routeTable: { + id: managedInstanceRouteTable.id + } + + delegations: [ + { + name: 'managed-instance-delegation' + properties: { + serviceName: 'Microsoft.Sql/managedInstances' + } + } + ] + } + } + { + name: 'Management' + properties: { + addressPrefix: managementSubnetPrefix + + networkSecurityGroup: { + id: vmNsg.id + } + } + } + { + name: 'TeamJumpServers' + properties: { + addressPrefix: teamSubnetPrefix + + networkSecurityGroup: { + id: vmNsg.id + } + } + } + { + name: 'AzureBastionSubnet' + properties: { + addressPrefix: bastionSubnetPrefix + } + } + { + name: 'PrivateEndpointsSubnet' + properties: { + addressPrefix: PrivateEndpointsSubnetPrefix + } + } + ] + } +} + +output vnetId string = vnet.id + +output managedInstanceSubnetId string = resourceId( + 'Microsoft.Network/virtualNetworks/subnets', + vnet.name, + 'ManagedInstance' +) + +output managementSubnetId string = resourceId( + 'Microsoft.Network/virtualNetworks/subnets', + vnet.name, + 'Management' +) + +output teamSubnetId string = resourceId( + 'Microsoft.Network/virtualNetworks/subnets', + vnet.name, + 'TeamJumpServers' +) + +output bastionSubnetId string = resourceId( + 'Microsoft.Network/virtualNetworks/subnets', + vnet.name, + 'AzureBastionSubnet' +) + +output PrivateEndpointsSubnetId string = resourceId( + 'Microsoft.Network/virtualNetworks/subnets', + vnet.name, + 'PrivateEndpointsSubnet' +) diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/sql2016-vm-cse.bicep b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/sql2016-vm-cse.bicep new file mode 100644 index 000000000..bda817c3f --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/sql2016-vm-cse.bicep @@ -0,0 +1,57 @@ +param location string +param vmName string +param storageAccountName string +param managedInstanceServer string +param repoBaseURL string + +param adminUsername string +@secure() +param adminPassword string + +param sqlMiAdminUsername string +@secure() +param sqlMiAdminPassword string + +var ConfigureSQLMachineCommand = 'powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "bootstrap-legacy.ps1" -BackupBaseUri "${repoBaseURL}/Databases" -adminUsername ${adminUsername} -adminPassword ${adminPassword} -sqlMiAdminUsername ${sqlMiAdminUsername} -sqlMiAdminPassword ${sqlMiAdminPassword} -StorageAccountName ${storageAccountName} -ManagedInstanceServer ${managedInstanceServer}' + +resource virtualMachine 'Microsoft.Compute/virtualMachines@2024-11-01' existing = { + name: vmName +} + +resource sqlVirtualMachine 'Microsoft.SqlVirtualMachine/sqlVirtualMachines@2023-10-01' existing = { + name: vmName +} + +resource ConfigureSQLMachine 'Microsoft.Compute/virtualMachines/extensions@2024-11-01' = { + parent: virtualMachine + name: 'ConfigureSQLMachine' + location: location + + dependsOn: [ + sqlVirtualMachine + ] + + properties: { + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.10' + autoUpgradeMinorVersion: true + + settings: { + fileUris: [ + '${repoBaseURL}/scripts/Set-FW-ForAllInstances.ps1' + //'${repoBaseURL}/scripts/Restore-TeamDatabases.ps1' + '${repoBaseURL}/scripts/Download-TeamDatabases.ps1' + '${repoBaseURL}/scripts/bootstrap-legacy.ps1' + '${repoBaseURL}/scripts/Restore-TeamDatabasesMI.ps1' + '${repoBaseURL}/scripts/Install-AzureCLI.ps1' + '${repoBaseURL}/scripts/Configure-SQLMI.ps1' + '${repoBaseURL}/scripts/Configure-legacySQL.ps1' + ] + } + + protectedSettings: { + commandToExecute: ConfigureSQLMachineCommand + } + } +} diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/sql2016-vm.bicep b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/sql2016-vm.bicep new file mode 100644 index 000000000..3129d1f17 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/sql2016-vm.bicep @@ -0,0 +1,129 @@ +param location string +param vmName string +param subnetId string +param privateIPAddress string +param vmSize string +param adminUsername string + +@secure() +param adminPassword string + +param tags object + +resource networkInterface 'Microsoft.Network/networkInterfaces@2024-05-01' = { + name: '${vmName}-nic' + location: location + tags: tags + + properties: { + ipConfigurations: [ + { + name: 'ipconfig1' + properties: { + privateIPAllocationMethod: 'Static' + privateIPAddress: privateIPAddress + + subnet: { + id: subnetId + } + } + } + ] + } +} + +resource virtualMachine 'Microsoft.Compute/virtualMachines@2024-11-01' = { + name: vmName + location: location + tags: tags + + identity: { + type: 'SystemAssigned' + } + + properties: { + hardwareProfile: { + vmSize: vmSize + } + + osProfile: { + computerName: vmName + adminUsername: adminUsername + adminPassword: adminPassword + + windowsConfiguration: { + provisionVMAgent: true + enableAutomaticUpdates: true + } + } + + storageProfile: { + imageReference: { + publisher: 'MicrosoftSQLServer' + offer: 'SQL2016SP3-WS2019' + sku: 'sqldev' + version: 'latest' + } + + osDisk: { + createOption: 'FromImage' + + managedDisk: { + storageAccountType: 'Premium_LRS' + } + } + } + + networkProfile: { + networkInterfaces: [ + { + id: networkInterface.id + } + ] + } + } +} + +resource autoShutdownConfig 'Microsoft.DevTestLab/schedules@2018-09-15' = { + name: 'shutdown-computevm-${vmName}' + location: location + properties: { + status: 'Enabled' + notificationSettings: { + status: 'Disabled' + timeInMinutes: 15 + notificationLocale: 'en' + } + dailyRecurrence: { + time: '1800' + } + timeZoneId: 'UTC' + taskType: 'ComputeVmShutdownTask' + targetResourceId: virtualMachine.id + } +} + +resource sqlVirtualMachine 'Microsoft.SqlVirtualMachine/sqlVirtualMachines@2023-10-01' = { + name: vmName + location: location + + properties: { + virtualMachineResourceId: virtualMachine.id + + sqlManagement: 'Full' + + serverConfigurationsManagementSettings: { + sqlConnectivityUpdateSettings: { + connectivityType: 'PRIVATE' + port: 1433 + + sqlAuthUpdateUserName: adminUsername + sqlAuthUpdatePassword: adminPassword + } + } + } +} + +output vmName string = virtualMachine.name +output privateIPAddress string = privateIPAddress +output vmPrincipalId string = virtualMachine.identity.principalId diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/sql2022-vm-cse.bicep b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/sql2022-vm-cse.bicep new file mode 100644 index 000000000..97fc162fe --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/sql2022-vm-cse.bicep @@ -0,0 +1,45 @@ +param location string +param vmName string +param repoBaseURL string + +param adminUsername string +@secure() +param adminPassword string + +var ConfigureSQLMachineCommand = 'powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "bootstrap-newsql.ps1" -BackupUri "https://github.com/Microsoft/sql-server-samples/releases/download/wide-world-importers-v1.0/WideWorldImporters-Full.bak" -adminUsername ${adminUsername} -adminPassword ${adminPassword}' + +resource virtualMachine 'Microsoft.Compute/virtualMachines@2024-11-01' existing = { + name: vmName +} + +resource sqlVirtualMachine 'Microsoft.SqlVirtualMachine/sqlVirtualMachines@2023-10-01' existing = { + name: vmName +} + +resource ConfigureSQLMachine 'Microsoft.Compute/virtualMachines/extensions@2024-11-01' = { + parent: virtualMachine + name: 'ConfigureSQLMachine' + location: location + + dependsOn: [ + sqlVirtualMachine + ] + properties: { + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.10' + autoUpgradeMinorVersion: true + + settings: { + fileUris: [ + '${repoBaseURL}/scripts/Set-FW-ForAllInstances.ps1' + '${repoBaseURL}/scripts/Restore-SampleDatabases.ps1' + '${repoBaseURL}/scripts/bootstrap-newsql.ps1' + ] + } + + protectedSettings: { + commandToExecute: ConfigureSQLMachineCommand + } + } +} diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/sql2022-vm.bicep b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/sql2022-vm.bicep new file mode 100644 index 000000000..f0364d542 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/sql2022-vm.bicep @@ -0,0 +1,124 @@ +param location string +param vmName string +param subnetId string +param privateIPAddress string +param vmSize string +param adminUsername string + +@secure() +param adminPassword string + +param tags object + +resource networkInterface 'Microsoft.Network/networkInterfaces@2024-05-01' = { + name: '${vmName}-nic' + location: location + tags: tags + + properties: { + ipConfigurations: [ + { + name: 'ipconfig1' + properties: { + privateIPAllocationMethod: 'Static' + privateIPAddress: privateIPAddress + + subnet: { + id: subnetId + } + } + } + ] + } +} + +resource virtualMachine 'Microsoft.Compute/virtualMachines@2024-11-01' = { + name: vmName + location: location + tags: tags + + properties: { + hardwareProfile: { + vmSize: vmSize + } + + osProfile: { + computerName: vmName + adminUsername: adminUsername + adminPassword: adminPassword + + windowsConfiguration: { + provisionVMAgent: true + enableAutomaticUpdates: true + } + } + + storageProfile: { + imageReference: { + publisher: 'MicrosoftSQLServer' + offer: 'SQL2022-WS2022' + sku: 'sqldev-gen2' + version: 'latest' + } + + osDisk: { + createOption: 'FromImage' + + managedDisk: { + storageAccountType: 'Standard_LRS' + } + } + } + + networkProfile: { + networkInterfaces: [ + { + id: networkInterface.id + } + ] + } + } +} + +resource autoShutdownConfig 'Microsoft.DevTestLab/schedules@2018-09-15' = { + name: 'shutdown-computevm-${vmName}' + location: location + properties: { + status: 'Enabled' + notificationSettings: { + status: 'Disabled' + timeInMinutes: 15 + notificationLocale: 'en' + } + dailyRecurrence: { + time: '1800' + } + timeZoneId: 'UTC' + taskType: 'ComputeVmShutdownTask' + targetResourceId: virtualMachine.id + } +} + +resource sqlVirtualMachine 'Microsoft.SqlVirtualMachine/sqlVirtualMachines@2023-10-01' = { + name: vmName + location: location + + properties: { + virtualMachineResourceId: virtualMachine.id + + sqlManagement: 'Full' + + serverConfigurationsManagementSettings: { + sqlConnectivityUpdateSettings: { + connectivityType: 'PRIVATE' + port: 1433 + + sqlAuthUpdateUserName: adminUsername + sqlAuthUpdatePassword: adminPassword + } + } + } +} + +output vmName string = virtualMachine.name +output privateIPAddress string = privateIPAddress diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/storage-rbac.bicep b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/storage-rbac.bicep new file mode 100644 index 000000000..e8f00159c --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/storage-rbac.bicep @@ -0,0 +1,68 @@ +param storageAccountName string +param vmPrincipalId string +param sqlmiPrincipalId string + +var storageBlobDataContributorRoleId = subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + 'ba92f5b4-2d11-453d-a403-e96b0029c9fe' +) + +/* +var storageBlobDataReaderRoleId = subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1' +) +*/ + +resource storageAccount 'Microsoft.Storage/storageAccounts@2025-01-01' existing = { + name: storageAccountName +} + +resource vmBlobContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid( + storageAccount.id, + vmPrincipalId, + storageBlobDataContributorRoleId + ) + + scope: storageAccount + + properties: { + roleDefinitionId: storageBlobDataContributorRoleId + principalId: vmPrincipalId + principalType: 'ServicePrincipal' + } +} + +/* resource sqlmiBlobDataReaderRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid( + storageAccount.id, + sqlmiPrincipalId, + storageBlobDataReaderRoleId + ) + + scope: storageAccount + + properties: { + roleDefinitionId: storageBlobDataReaderRoleId + principalId: sqlmiPrincipalId + principalType: 'ServicePrincipal' + } +} + */ + +resource sqlmiBlobContributorRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid( + storageAccount.id, + sqlmiPrincipalId, + storageBlobDataContributorRoleId + ) + + scope: storageAccount + + properties: { + roleDefinitionId: storageBlobDataContributorRoleId + principalId: sqlmiPrincipalId + principalType: 'ServicePrincipal' + } +} diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/storage.bicep b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/storage.bicep new file mode 100644 index 000000000..687ff0583 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/storage.bicep @@ -0,0 +1,105 @@ +param location string +param storageAccountName string +param tags object +param privateEndpointName string +param privateEndpointSubnetId string +param vnetId string + +resource storageAccount 'Microsoft.Storage/storageAccounts@2025-01-01' = { + name: storageAccountName + location: location + tags: tags + + sku: { + name: 'Standard_LRS' + } + + kind: 'StorageV2' + + properties: { + accessTier: 'Hot' + allowBlobPublicAccess: false + allowSharedKeyAccess: true + minimumTlsVersion: 'TLS1_2' + supportsHttpsTrafficOnly: true + } +} + +resource backupContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2025-01-01' = { + name: '${storageAccount.name}/default/backups' + + properties: { + publicAccess: 'None' + } +} + +resource auditContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2025-01-01' = { + name: '${storageAccount.name}/default/auditlogs' + + properties: { + publicAccess: 'None' + } +} + +resource storagePrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' = { + name: privateEndpointName + location: location + + properties: { + subnet: { + id: privateEndpointSubnetId + } + + privateLinkServiceConnections: [ + { + name: 'blobConnection' + + properties: { + privateLinkServiceId: storageAccount.id + groupIds: [ + 'blob' + ] + } + } + ] + } +} + +resource blobDnsZone 'Microsoft.Network/privateDnsZones@2024-06-01' = { + name: 'privatelink.blob.${environment().suffixes.storage}' + location: 'global' +} + +resource dnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = { + name: 'vnet-link' + parent: blobDnsZone + location: 'global' + + properties: { + registrationEnabled: false + + virtualNetwork: { + id: vnetId + } + } +} + +resource storagePrivateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = { + name: 'default' + parent: storagePrivateEndpoint + + properties: { + privateDnsZoneConfigs: [ + { + name: 'blob' + + properties: { + privateDnsZoneId: blobDnsZone.id + } + } + ] + } +} + +output storageAccountName string = storageAccount.name +output blobEndpoint string = storageAccount.properties.primaryEndpoints.blob diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/team-vm-cse.bicep b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/team-vm-cse.bicep new file mode 100644 index 000000000..0f5b85714 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/team-vm-cse.bicep @@ -0,0 +1,47 @@ +param location string +param repoBaseURL string +param managedInstanceServer string +param storageAccountName string +param vmName string +param TeamName string = 'TEAM01' +param legacySQLName string +param adminUsername string +@secure() +param adminPassword string + +//var ConfigureTeamVMCommand = 'powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "bootstrap-teamvm.ps1" -SamplesBaseUri "${repoBaseURL}/TSQL_Scripts" -WallpaperUri "${repoBaseURL}/assets/BaseWallpaper.jpg" -TeamNumber ##teamNumber##' +var ConfigureTeamVMCommand = 'powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "bootstrap-teamvm.ps1" -SamplesBaseUri "${repoBaseURL}/TSQL_Scripts" -WallpaperUri "${repoBaseURL}/assets/BaseWallpaper.jpg" -TeamName ${TeamName} -LabsBaseUri "${repoBaseURL}/LABS" -ManagedInstanceServer "${managedInstanceServer}" -StorageAccountName "${storageAccountName}" -BackupBaseUri "${repoBaseURL}/Databases" -ServerInstance "${legacySQLName}" -adminUsername "${adminUsername}" -adminPassword "${adminPassword}"' +resource virtualMachine 'Microsoft.Compute/virtualMachines@2024-11-01' existing = { + name: vmName +} + +resource ConfigureTeamMachine 'Microsoft.Compute/virtualMachines/extensions@2024-11-01' = { + parent: virtualMachine + name: 'ConfigureTeamMachine' + location: location + + properties: { + publisher: 'Microsoft.Compute' + type: 'CustomScriptExtension' + typeHandlerVersion: '1.10' + autoUpgradeMinorVersion: true + + settings: { + fileUris: [ + '${repoBaseURL}/scripts/install-team-tools.ps1' + '${repoBaseURL}/scripts/Download-Samples.ps1' + '${repoBaseURL}/scripts/bootstrap-teamvm.ps1' + '${repoBaseURL}/scripts/Configure-Teams-Shortcuts.ps1' + '${repoBaseURL}/scripts/Restore-TeamDatabases.ps1' + '${repoBaseURL}/scripts/Configure-legacySQL-DB.ps1' + '${repoBaseURL}/scripts/Download-Labs.ps1' + '${repoBaseURL}/scripts/Configure-TeamWallpaper.ps1' + ] + } + + protectedSettings: { + //commandToExecute: replace(ConfigureTeamVMCommand, '##teamNumber##', string(index + 1)) + commandToExecute: ConfigureTeamVMCommand + } + } +} diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/team-vm.bicep b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/team-vm.bicep new file mode 100644 index 000000000..2a24da620 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/infra/modules/team-vm.bicep @@ -0,0 +1,108 @@ +param location string + +param subnetId string +param vmSize string +param vmName string + +param adminUsername string +@secure() +param adminPassword string + +param tags object + +resource networkInterfaces 'Microsoft.Network/networkInterfaces@2024-05-01' = { + name: '${vmName}-nic' + location: location + tags: tags + + properties: { + ipConfigurations: [ + { + name: 'ipconfig1' + properties: { + privateIPAllocationMethod: 'Dynamic' + + subnet: { + id: subnetId + } + } + } + ] + } +} + +resource virtualMachine 'Microsoft.Compute/virtualMachines@2024-11-01' = { + name: vmName + location: location + + tags: tags + + properties: { + hardwareProfile: { + vmSize: vmSize + } + + osProfile: { + computerName: vmName + adminUsername: adminUsername + adminPassword: adminPassword + + windowsConfiguration: { + provisionVMAgent: true + enableAutomaticUpdates: true + } + } + + storageProfile: { + // imageReference: { + // publisher: 'MicrosoftWindowsServer' + // offer: 'WindowsServer' + // sku: '2022-datacenter-g2' + // version: 'latest' + // } + imageReference: { + publisher: 'MicrosoftWindowsDesktop' + offer: 'windows-11' + sku: 'win11-23h2-ent' + version: 'latest' + } + + osDisk: { + createOption: 'FromImage' + + managedDisk: { + storageAccountType: 'StandardSSD_LRS' + } + } + } + + networkProfile: { + networkInterfaces: [ + { + id: networkInterfaces.id + } + ] + } + } +} + +resource autoShutdownConfigs 'Microsoft.DevTestLab/schedules@2018-09-15' = { + name: 'shutdown-computevm-${vmName}' + location: location + properties: { + status: 'Enabled' + notificationSettings: { + status: 'Disabled' + timeInMinutes: 15 + notificationLocale: 'en' + } + dailyRecurrence: { + time: '1800' + } + timeZoneId: 'UTC' + taskType: 'ComputeVmShutdownTask' + targetResourceId: virtualMachine.id + } +} + +output vmName string = virtualMachine.name diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/lab-defaults.json b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/lab-defaults.json new file mode 100644 index 000000000..6fb33105b --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/lab-defaults.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/MicroHack/refs/heads/main/lab-defaults-schema.json", + "groups": [ "GHCPUsers" ], + "deploymentType": "resourcegroup", + "labsPerSubscription": 40, + "preferredLocation": "swedencentral, norwayeast, spaincentral", + "estimatedDailyCostsUsd": 5.0, + "estimatedSharedDeploymentDailyCostsUsd": 50.0 +} \ No newline at end of file diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Configure-SQLMI.ps1 b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Configure-SQLMI.ps1 new file mode 100644 index 000000000..615564e65 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Configure-SQLMI.ps1 @@ -0,0 +1,515 @@ +param( + [Parameter(Mandatory)] + [string]$ManagedInstanceServer, + + [Parameter(Mandatory)] + [string] $sqlusername, + + [Parameter(Mandatory)] + [string] $sqlpassword +) + +$ErrorActionPreference = 'Stop' + +$logPath = 'C:\Windows\Temp\Configure-SQLMI.log' +Start-Transcript -Path $logPath -Append + +Write-Host 'Configuring SQL Managed Instance...' + +$ConfigureSql = @" +USE master +GO +EXEC sp_configure 'CLR Enabled', 1 +RECONFIGURE WITH OVERRIDE +GO + +USE [msdb] +GO +IF EXISTS (SELECT job_id + FROM msdb.dbo.sysjobs_view + WHERE name = N'Workload01') +EXEC msdb.dbo.sp_delete_job @job_name=N'Workload01' + , @delete_unused_schedule=1 + +IF EXISTS (SELECT job_id + FROM msdb.dbo.sysjobs_view + WHERE name = N'Workload02') +EXEC msdb.dbo.sp_delete_job @job_name=N'Workload02' + , @delete_unused_schedule=1 +GO +BEGIN TRANSACTION +DECLARE @ReturnCode INT +SELECT @ReturnCode = 0 + +IF NOT EXISTS (SELECT name FROM msdb.dbo.syscategories WHERE name=N'[Uncategorized (Local)]' AND category_class=1) +BEGIN +EXEC @ReturnCode = msdb.dbo.sp_add_category @class=N'JOB', @type=N'LOCAL', @name=N'[Uncategorized (Local)]' +IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback + +END + +DECLARE @jobId BINARY(16) +EXEC @ReturnCode = msdb.dbo.sp_add_job @job_name=N'Workload01', + @enabled=1, + @notify_level_eventlog=0, + @notify_level_email=0, + @notify_level_netsend=0, + @notify_level_page=0, + @delete_level=0, + @description=N'No description available.', + @category_name=N'[Uncategorized (Local)]', + @owner_login_name=N'DemoUser', @job_id = @jobId OUTPUT +IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback + +EXEC @ReturnCode = msdb.dbo.sp_add_jobstep @job_id=@jobId, @step_name=N'Step1', + @step_id=1, + @cmdexec_success_code=0, + @on_success_action=1, + @on_success_step_id=0, + @on_fail_action=2, + @on_fail_step_id=0, + @retry_attempts=0, + @retry_interval=0, + @os_run_priority=0, @subsystem=N'TSQL', + @command=N' + DECLARE @i INT=0 + WHILE @i<100 + BEGIN + EXEC GetTransactionTotalPerReferenceOrderID 41590 + EXEC uspGetManagerEmployees 263 + EXEC [dbo].[uspGetEmployeeManagers] 264 + EXEC [dbo].[uspGetBillOfMaterials] @StartProductID = 517, @CheckDate= ''2010-11-14 00:00:00.000'' + EXEC [dbo].[uspGetWhereUsedProductID] 749,@CheckDate= ''2010-11-14 00:00:00.000'' + SET @i =@i+1 + END + WAITFOR DELAY ''00:01'' + + SET @i =0 + WHILE @i<500 + BEGIN + EXEC uspGetManagerEmployees 263 + EXEC [dbo].[uspGetEmployeeManagers] 264 + EXEC [dbo].[uspGetBillOfMaterials] @StartProductID = 517, @CheckDate= ''2010-11-14 00:00:00.000'' + EXEC [dbo].[uspGetWhereUsedProductID] 749,@CheckDate= ''2010-11-14 00:00:00.000'' + SET @i =@i+1 + END + WAITFOR DELAY ''00:02:30'' + SET @i =0 + WHILE @i<100 + BEGIN + EXEC GetTransactionTotalPerReferenceOrderID 41590 + SET @i =@i+1 + END +WAITFOR DELAY ''00:01'' +SET @i =0 + WHILE @i<1000 + BEGIN + EXEC uspGetManagerEmployees 263 + EXEC [dbo].[uspGetEmployeeManagers] 264 + EXEC [dbo].[uspGetBillOfMaterials] @StartProductID = 517, @CheckDate= ''2010-11-14 00:00:00.000'' + EXEC [dbo].[uspGetWhereUsedProductID] 749,@CheckDate= ''2010-11-14 00:00:00.000'' + SET @i =@i+1 + END +WAITFOR DELAY ''00:01'' +SET @i =0 + WHILE @i<100 + BEGIN + EXEC GetTransactionTotalPerReferenceOrderID 41590 + EXEC uspGetManagerEmployees 263 + EXEC [dbo].[uspGetEmployeeManagers] 264 + EXEC [dbo].[uspGetBillOfMaterials] @StartProductID = 517, @CheckDate= ''2010-11-14 00:00:00.000'' + EXEC [dbo].[uspGetWhereUsedProductID] 749,@CheckDate= ''2010-11-14 00:00:00.000'' + SET @i =@i+1 + END + WHILE @i<100 + BEGIN + EXEC GetTransactionTotalPerReferenceOrderID 41590 + EXEC uspGetManagerEmployees 263 + EXEC [dbo].[uspGetEmployeeManagers] 264 + EXEC [dbo].[uspGetBillOfMaterials] @StartProductID = 517, @CheckDate= ''2010-11-14 00:00:00.000'' + EXEC [dbo].[uspGetWhereUsedProductID] 749,@CheckDate= ''2010-11-14 00:00:00.000'' + SET @i =@i+1 + END + WAITFOR DELAY ''00:01'' + + SET @i =0 + WHILE @i<500 + BEGIN + EXEC uspGetManagerEmployees 263 + EXEC [dbo].[uspGetEmployeeManagers] 264 + EXEC [dbo].[uspGetBillOfMaterials] @StartProductID = 517, @CheckDate= ''2010-11-14 00:00:00.000'' + EXEC [dbo].[uspGetWhereUsedProductID] 749,@CheckDate= ''2010-11-14 00:00:00.000'' + SET @i =@i+1 + END + WAITFOR DELAY ''00:02:30'' + SET @i =0 + WHILE @i<100 + BEGIN + EXEC GetTransactionTotalPerReferenceOrderID 41590 + SET @i =@i+1 + END +WAITFOR DELAY ''00:02'' +SET @i =0 + WHILE @i<1000 + BEGIN + EXEC uspGetManagerEmployees 263 + EXEC [dbo].[uspGetEmployeeManagers] 264 + EXEC [dbo].[uspGetBillOfMaterials] @StartProductID = 517, @CheckDate= ''2010-11-14 00:00:00.000'' + EXEC [dbo].[uspGetWhereUsedProductID] 749,@CheckDate= ''2010-11-14 00:00:00.000'' + SET @i =@i+1 + END +WAITFOR DELAY ''00:01'' +SET @i =0 + WHILE @i<100 + BEGIN + EXEC GetTransactionTotalPerReferenceOrderID 41590 + EXEC uspGetManagerEmployees 263 + EXEC [dbo].[uspGetEmployeeManagers] 264 + EXEC [dbo].[uspGetBillOfMaterials] @StartProductID = 517, @CheckDate= ''2010-11-14 00:00:00.000'' + EXEC [dbo].[uspGetWhereUsedProductID] 749,@CheckDate= ''2010-11-14 00:00:00.000'' + SET @i =@i+1 + END +WAITFOR DELAY ''00:04'' + + SET @i =0 + WHILE @i<100 + BEGIN + EXEC GetTransactionTotalPerReferenceOrderID 41590 + EXEC uspGetManagerEmployees 263 + EXEC [dbo].[uspGetEmployeeManagers] 264 + EXEC [dbo].[uspGetBillOfMaterials] @StartProductID = 517, @CheckDate= ''2010-11-14 00:00:00.000'' + EXEC [dbo].[uspGetWhereUsedProductID] 749,@CheckDate= ''2010-11-14 00:00:00.000'' + SET @i =@i+1 + END + WAITFOR DELAY ''00:01'' + + SET @i =0 + WHILE @i<500 + BEGIN + EXEC uspGetManagerEmployees 263 + EXEC [dbo].[uspGetEmployeeManagers] 264 + EXEC [dbo].[uspGetBillOfMaterials] @StartProductID = 517, @CheckDate= ''2010-11-14 00:00:00.000'' + EXEC [dbo].[uspGetWhereUsedProductID] 749,@CheckDate= ''2010-11-14 00:00:00.000'' + SET @i =@i+1 + END + WAITFOR DELAY ''00:02:30'' + SET @i =0 + WHILE @i<100 + BEGIN + EXEC GetTransactionTotalPerReferenceOrderID 41590 + SET @i =@i+1 + END +WAITFOR DELAY ''00:01'' +SET @i =0 + WHILE @i<1000 + BEGIN + EXEC uspGetManagerEmployees 263 + EXEC [dbo].[uspGetEmployeeManagers] 264 + EXEC [dbo].[uspGetBillOfMaterials] @StartProductID = 517, @CheckDate= ''2010-11-14 00:00:00.000'' + EXEC [dbo].[uspGetWhereUsedProductID] 749,@CheckDate= ''2010-11-14 00:00:00.000'' + SET @i =@i+1 + END +WAITFOR DELAY ''00:01'' +SET @i =0 + WHILE @i<100 + BEGIN + EXEC GetTransactionTotalPerReferenceOrderID 41590 + EXEC uspGetManagerEmployees 263 + EXEC [dbo].[uspGetEmployeeManagers] 264 + EXEC [dbo].[uspGetBillOfMaterials] @StartProductID = 517, @CheckDate= ''2010-11-14 00:00:00.000'' + EXEC [dbo].[uspGetWhereUsedProductID] 749,@CheckDate= ''2010-11-14 00:00:00.000'' + SET @i =@i+1 + END + WHILE @i<100 + BEGIN + EXEC GetTransactionTotalPerReferenceOrderID 41590 + EXEC uspGetManagerEmployees 263 + EXEC [dbo].[uspGetEmployeeManagers] 264 + EXEC [dbo].[uspGetBillOfMaterials] @StartProductID = 517, @CheckDate= ''2010-11-14 00:00:00.000'' + EXEC [dbo].[uspGetWhereUsedProductID] 749,@CheckDate= ''2010-11-14 00:00:00.000'' + SET @i =@i+1 + END + WAITFOR DELAY ''00:01'' + + SET @i =0 + WHILE @i<500 + BEGIN + EXEC uspGetManagerEmployees 263 + EXEC [dbo].[uspGetEmployeeManagers] 264 + EXEC [dbo].[uspGetBillOfMaterials] @StartProductID = 517, @CheckDate= ''2010-11-14 00:00:00.000'' + EXEC [dbo].[uspGetWhereUsedProductID] 749,@CheckDate= ''2010-11-14 00:00:00.000'' + SET @i =@i+1 + END + WAITFOR DELAY ''00:02:30'' + SET @i =0 + WHILE @i<100 + BEGIN + EXEC GetTransactionTotalPerReferenceOrderID 41590 + SET @i =@i+1 + END +WAITFOR DELAY ''00:04'' +SET @i =0 + WHILE @i<1000 + BEGIN + EXEC uspGetManagerEmployees 263 + EXEC [dbo].[uspGetEmployeeManagers] 264 + EXEC [dbo].[uspGetBillOfMaterials] @StartProductID = 517, @CheckDate= ''2010-11-14 00:00:00.000'' + EXEC [dbo].[uspGetWhereUsedProductID] 749,@CheckDate= ''2010-11-14 00:00:00.000'' + SET @i =@i+1 + END +WAITFOR DELAY ''00:01'' +SET @i =0 + WHILE @i<100 + BEGIN + EXEC GetTransactionTotalPerReferenceOrderID 41590 + EXEC uspGetManagerEmployees 263 + EXEC [dbo].[uspGetEmployeeManagers] 264 + EXEC [dbo].[uspGetBillOfMaterials] @StartProductID = 517, @CheckDate= ''2010-11-14 00:00:00.000'' + EXEC [dbo].[uspGetWhereUsedProductID] 749,@CheckDate= ''2010-11-14 00:00:00.000'' + SET @i =@i+1 + END +', + @database_name=N'TenantCRM', + @flags=0 +IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback +EXEC @ReturnCode = msdb.dbo.sp_update_job @job_id = @jobId, @start_step_id = 1 +IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback +EXEC @ReturnCode = msdb.dbo.sp_add_jobserver @job_id = @jobId, @server_name = N'(local)' +IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback +COMMIT TRANSACTION +GOTO EndSave +QuitWithRollback: + IF (@@TRANCOUNT > 0) ROLLBACK TRANSACTION +EndSave: +GO + +USE [msdb] +GO + +/****** Object: Job [Workload02] ******/ +BEGIN TRANSACTION +DECLARE @ReturnCode INT +SELECT @ReturnCode = 0 + +IF NOT EXISTS (SELECT name FROM msdb.dbo.syscategories WHERE name=N'[Uncategorized (Local)]' AND category_class=1) +BEGIN +EXEC @ReturnCode = msdb.dbo.sp_add_category @class=N'JOB', @type=N'LOCAL', @name=N'[Uncategorized (Local)]' +IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback + +END + +DECLARE @jobId BINARY(16) +EXEC @ReturnCode = msdb.dbo.sp_add_job @job_name=N'Workload02', + @enabled=1, + @notify_level_eventlog=0, + @notify_level_email=0, + @notify_level_netsend=0, + @notify_level_page=0, + @delete_level=0, + @description=N'No description available.', + @category_name=N'[Uncategorized (Local)]', + @owner_login_name=N'DemoUser', @job_id = @jobId OUTPUT +IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback + +EXEC @ReturnCode = msdb.dbo.sp_add_jobstep @job_id=@jobId, @step_name=N'Step1', + @step_id=1, + @cmdexec_success_code=0, + @on_success_action=1, + @on_success_step_id=0, + @on_fail_action=2, + @on_fail_step_id=0, + @retry_attempts=0, + @retry_interval=0, + @os_run_priority=0, @subsystem=N'TSQL', + @command=N'WAITFOR DELAY ''00:01'' +GO +DECLARE @i INT=0 + WHILE @i<500 + BEGIN + EXEC GetTransactionTotalPerReferenceOrderID 41590 + SET @i =@i+1 + END +GO + WAITFOR DELAY ''00:00:30'' + GO + DECLARE @i INT=0 + WHILE @i<300 + BEGIN + EXEC GetTransactionTotalPerReferenceOrderID 41590 + SET @i =@i+1 + END +GO + WAITFOR DELAY ''00:01:30'' + GO + DECLARE @i INT=0 + WHILE @i<200 + BEGIN + EXEC GetTransactionTotalPerReferenceOrderID 41590 + SET @i =@i+1 + END +GO + WAITFOR DELAY ''00:02:30'' +GO +DECLARE @i INT=0 + WHILE @i<500 + BEGIN + EXEC GetTransactionTotalPerReferenceOrderID 41590 + SET @i =@i+1 + END +GO + WAITFOR DELAY ''00:00:30'' + GO + DECLARE @i INT=0 + WHILE @i<300 + BEGIN + EXEC GetTransactionTotalPerReferenceOrderID 41590 + SET @i =@i+1 + END +GO + WAITFOR DELAY ''00:01:30'' + GO + DECLARE @i INT=0 + WHILE @i<200 + BEGIN + EXEC GetTransactionTotalPerReferenceOrderID 41590 + SET @i =@i+1 + END +GO + WAITFOR DELAY ''00:02:30'' +GO +DECLARE @i INT=0 + WHILE @i<500 + BEGIN + EXEC GetTransactionTotalPerReferenceOrderID 41590 + SET @i =@i+1 + END +GO + WAITFOR DELAY ''00:00:30'' + GO + DECLARE @i INT=0 + WHILE @i<300 + BEGIN + EXEC GetTransactionTotalPerReferenceOrderID 41590 + SET @i =@i+1 + END +GO + WAITFOR DELAY ''00:01:30'' + GO + DECLARE @i INT=0 + WHILE @i<200 + BEGIN + EXEC GetTransactionTotalPerReferenceOrderID 41590 + SET @i =@i+1 + END +GO + WAITFOR DELAY ''00:02:30'' +GO +DECLARE @i INT=0 + WHILE @i<500 + BEGIN + EXEC GetTransactionTotalPerReferenceOrderID 41590 + SET @i =@i+1 + END +GO + WAITFOR DELAY ''00:00:30'' + GO + DECLARE @i INT=0 + WHILE @i<300 + BEGIN + EXEC GetTransactionTotalPerReferenceOrderID 41590 + SET @i =@i+1 + END +GO + WAITFOR DELAY ''00:01:30'' + GO + DECLARE @i INT=0 + WHILE @i<200 + BEGIN + EXEC GetTransactionTotalPerReferenceOrderID 41590 + SET @i =@i+1 + END +', + @database_name=N'TenantCRM', + @flags=0 +IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback +EXEC @ReturnCode = msdb.dbo.sp_update_job @job_id = @jobId, @start_step_id = 1 +IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback +EXEC @ReturnCode = msdb.dbo.sp_add_jobserver @job_id = @jobId, @server_name = N'(local)' +IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback +COMMIT TRANSACTION +GOTO EndSave +QuitWithRollback: + IF (@@TRANCOUNT > 0) ROLLBACK TRANSACTION +EndSave: +GO +DECLARE @schedule_id int +EXEC msdb.dbo.sp_add_jobschedule @Job_name='Workload01', @name=N'Perf_Scheduled', + @enabled=1, + @freq_type=4, + @freq_interval=1, + @freq_subday_type=1, + @freq_subday_interval=0, + @freq_relative_interval=0, + @freq_recurrence_factor=1, + @active_start_date=20211116, + @active_end_date=99991231, + @active_start_time=112500, + @active_end_time=235959, @schedule_id = @schedule_id OUTPUT + +EXEC msdb.dbo.sp_attach_schedule @Job_name='Workload02',@schedule_id=@schedule_id +GO +USE [TenantCRM] +GO +CREATE OR ALTER PROC dbo.AdminExecuteSQLCommand + @ExecuteText nvarchar(MAX) = NULL -- NULL default value +AS + SET NOCOUNT ON; + IF LEN(ISNULL(@ExecuteText,'')) > 0 + EXEC (@ExecuteText) +GO +"@ + +[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.ConnectionInfo") | Out-Null +[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo") | Out-Null + +$connectionString = "Data Source=$ManagedInstanceServer;Initial Catalog=master;TrustServerCertificate=True;" +$Connection = New-Object System.Data.SqlClient.SqlConnection($connectionString) +[System.Security.SecureString]$SQLPwd = $sqlpassword | ConvertTo-SecureString -AsPlainText -Force +$SQLPwd.MakeReadOnly() +$cred = New-Object System.Data.SqlClient.SqlCredential($sqlusername,$SQLPwd) +$Connection.credential = $cred +#$Connection.open() + +#$command = New-Object system.Data.SqlClient.SqlCommand($Connection) +#$command.Connection = $Connection +#$command.CommandTimeout = $QueryTimeout + +#$command.CommandText = $ConfigureSql +#$result = $command.ExecuteReader() +#$table = New-Object System.Data.DataTable +#$table.Load($result) + +# 5. SMO-Serververbindung initialisieren +$serverConnection = New-Object Microsoft.SqlServer.Management.Common.ServerConnection($Connection) +$server = New-Object Microsoft.SqlServer.Management.Smo.Server($serverConnection) + +try { + # Ausführung des Skripts inkl. GO-Trenner + [void]$server.ConnectionContext.ExecuteNonQuery($ConfigureSql) + #Write-Host "Skript erfolgreich auf SQL MI ausgeführt." -ForegroundColor Green +} +catch { + Write-Error "Fehler bei der Ausführung: $_" +} +finally { + if ($null -ne $serverConnection -and $serverConnection.IsOpen) { + $serverConnection.Disconnect() + } +} +try { + $Connection.Close() +} +catch { +} + +Stop-Transcript \ No newline at end of file diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Configure-TeamWallpaper.ps1 b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Configure-TeamWallpaper.ps1 new file mode 100644 index 000000000..9595cb30e --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Configure-TeamWallpaper.ps1 @@ -0,0 +1,84 @@ +[CmdletBinding()] +param +( + [string] $WallpaperUri, + [string] $TeamName = "TEAM01" +) + +$ErrorActionPreference = 'Stop' + +$logPath = 'C:\Windows\Temp\Configure-TeamWallpaper.log' +Start-Transcript -Path $logPath -Append + +$DownloadDirectory = "C:\MicroHack" +$DownloadBaseWallpaperPath = Join-Path $DownloadDirectory "BaseWallpaper.jpg" +$TeamWallpaperPath = Join-Path $DownloadDirectory "Wallpaper.jpg" +#[string]$TeamNumberStr = '{0:d2}' -f $TeamNumber + +if (-not (Test-Path -LiteralPath "C:\MicroHack")) +{ + New-Item ` + -ItemType Directory ` + -Path "C:\MicroHack" ` + -Force | + Out-Null +} + +if (Test-Path $DownloadBaseWallpaperPath) { + Remove-Item -LiteralPath $DownloadBaseWallpaperPath -ErrorAction SilentlyContinue -Force +} + + +try { + Write-Host "Downloading wallpaper from $WallpaperUri..." + + $lastProgressPreference = $ProgressPreference + $ProgressPreference = 'SilentlyContinue' + Invoke-WebRequest -Uri $WallpaperUri -OutFile $DownloadBaseWallpaperPath -UseBasicParsing + $ProgressPreference = $lastProgressPreference + + Write-Host "Setting wallpaper for $TeamNName ..." + Add-Type -AssemblyName System.Drawing + + $img = [System.Drawing.Image]::FromFile($DownloadBaseWallpaperPath) + $bmp = New-Object System.Drawing.Bitmap($img) + + $graphics = [System.Drawing.Graphics]::FromImage($bmp) + + $font = New-Object System.Drawing.Font( + "Segoe UI", + 50, + [System.Drawing.FontStyle]::Bold + ) + + $brush = New-Object System.Drawing.SolidBrush( + [System.Drawing.Color]::White + ) + + $graphics.DrawString( +# "TEAM $TeamNumberStr", + $TeamNName, + $font, + $brush, + 1460, + 1100 + ) + + $bmp.Save( + $TeamWallpaperPath, + [System.Drawing.Imaging.ImageFormat]::Jpeg + ) + + $graphics.Dispose() + $bmp.Dispose() + $img.Dispose() + + New-Item "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System" -ErrorAction SilentlyContinue -Force + Set-ItemProperty "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System" -Name Wallpaper -Value "C:\MicroHack\Wallpaper.jpg" -ErrorAction SilentlyContinue + Set-ItemProperty "HKLM:\Software\Microsoft\Windows\CurrentVersion\Policies\System" -Name WallpaperStyle -Value "10" -ErrorAction SilentlyContinue +} +catch { + Write-Host "An error occurred while configuring the team wallpaper: $_" +} + +Stop-Transcript \ No newline at end of file diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Configure-Teams-Shortcuts.ps1 b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Configure-Teams-Shortcuts.ps1 new file mode 100644 index 000000000..6bad7ea47 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Configure-Teams-Shortcuts.ps1 @@ -0,0 +1,68 @@ +[CmdletBinding()] +param +( + [string] $ManagedInstanceServer, + [string] $StorageAccountName +) + +$ErrorActionPreference = 'Stop' + +$logPath = 'C:\Windows\Temp\Configure-Teams-Shortcuts.log' +Start-Transcript -Path $logPath -Append + +if (-not (Test-Path -LiteralPath "C:\MicroHack")) +{ + New-Item ` + -ItemType Directory ` + -Path "C:\MicroHack" ` + -Force | + Out-Null +} + +$EnvironmentInfoPath = Join-Path "C:\MicroHack" "Readme.txt" + +if (Test-Path $EnvironmentInfoPath) { + Remove-Item -LiteralPath $EnvironmentInfoPath -ErrorAction SilentlyContinue -Force +} + +"Your Hack-Environment" | Out-File -FilePath $EnvironmentInfoPath -Encoding utf8 -Force +"---------------------------------------------------------------------------" | Out-File -FilePath $EnvironmentInfoPath -Encoding utf8 -Force -Append +"SQL Server 2016: legacysql2016" | Out-File -FilePath $EnvironmentInfoPath -Encoding utf8 -Force -Append +"SQL Server (Auth): Windows Authentication" | Out-File -FilePath $EnvironmentInfoPath -Encoding utf8 -Force -Append +"" | Out-File -FilePath $EnvironmentInfoPath -Encoding utf8 -Force -Append +"SQL Managed Instance: $ManagedInstanceServer" | Out-File -FilePath $EnvironmentInfoPath -Encoding utf8 -Force -Append +"SQL MI (Auth): SQL Authentication" | Out-File -FilePath $EnvironmentInfoPath -Encoding utf8 -Force -Append +"SQL MI (Username): DemoUser" | Out-File -FilePath $EnvironmentInfoPath -Encoding utf8 -Force -Append +"SQL MI (Password): Demo@pass1234567" | Out-File -FilePath $EnvironmentInfoPath -Encoding utf8 -Force -Append +"" | Out-File -FilePath $EnvironmentInfoPath -Encoding utf8 -Force -Append +"Storage Account: $StorageAccountName" | Out-File -FilePath $EnvironmentInfoPath -Encoding utf8 -Force -Append + +$TargetFile = $EnvironmentInfoPath +$ShortcutPath = "$env:Public\Desktop\Readme.lnk" + +$WshShell = New-Object -ComObject WScript.Shell +$Shortcut = $WshShell.CreateShortcut($ShortcutPath) +$Shortcut.TargetPath = $TargetFile +$Shortcut.WorkingDirectory = Split-Path $TargetFile +$Shortcut.IconLocation = "$env:SystemRoot\System32\shell32.dll,0" +$Shortcut.Save() + +$Target = "C:\Program Files\Microsoft SQL Server Management Studio 22\Release\Common7\IDE\SSMS.exe" +if (Test-Path $Target) { + $Shell = New-Object -ComObject WScript.Shell + $Link = $Shell.CreateShortcut("$env:Public\Desktop\SSMS 22.lnk") + $Link.TargetPath = $Target + $Link.IconLocation = "$Target,0" + $Link.Save() +} + +$Target = "C:\MicroHack\Labs" +if (Test-Path $Target) { + $Shell = New-Object -ComObject WScript.Shell + $Link = $Shell.CreateShortcut("$env:Public\Desktop\Labs.lnk") + $Link.TargetPath = $Target + $Link.IconLocation = "$Target,0" + $Link.Save() +} + +Stop-Transcript \ No newline at end of file diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Configure-legacySQL-DB.ps1 b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Configure-legacySQL-DB.ps1 new file mode 100644 index 000000000..b5c73b668 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Configure-legacySQL-DB.ps1 @@ -0,0 +1,87 @@ +param( + [Parameter(Mandatory)] + [string] $sqlusername, + [Parameter(Mandatory)] + [string] $sqlpassword, + [string] $ServerInstance = "localhost", + [string]$TeamName = 'TEAM01' +) + +$ErrorActionPreference = 'Stop' + +$logPath = 'C:\Windows\Temp\Configure-legacySQL-DB.log' +Start-Transcript -Path $logPath -Append + +Write-Host 'Configuring SQL legacy Instance DB...' + +$ConfigureSql = @" +DECLARE @DefaultLogPath NVARCHAR(500) +SELECT + @DefaultLogPath = LEFT(physical_name, LEN(physical_name) - CHARINDEX('\', REVERSE(physical_name)) + 1) +FROM + sys.master_files +WHERE + database_id = DB_ID('$($TeamName)_TenantDataDB') + AND file_id = 2; + +DECLARE @DBName VARCHAR(255) +DECLARE @SQLCmd NVARCHAR(MAX) +DECLARE DB_Crs CURSOR READ_ONLY FORWARD_ONLY FOR SELECT name FROM sys.databases WHERE name = '$($TeamName)_TenantDataDB' AND state_desc = 'ONLINE' +OPEN DB_Crs +FETCH NEXT FROM DB_Crs INTO @DBName + +WHILE @@FETCH_STATUS = 0 +BEGIN + SET @SQLCmd = 'USE [' + @DBName + '] + ALTER ASSEMBLY CLRUFDS WITH PERMISSION_SET = UNSAFE; + ALTER ASSEMBLY Database1 WITH PERMISSION_SET = UNSAFE; + ' + --PRINT @SQLCmd + EXEC (@SQLCmd) + + SET @SQLCmd = 'ALTER DATABASE [' + @DBName + '] + ADD LOG FILE (NAME = N''' + @DBName + '_Log2'', FILENAME = N''' + @DefaultLogPath + @DBName + '_Log2.ldf''); + ' + --PRINT @SQLCmd + EXEC (@SQLCmd) + + FETCH NEXT FROM DB_Crs INTO @DBName +END +CLOSE DB_Crs +DEALLOCATE DB_Crs +"@ + +$connectionString = "Data Source=$ServerInstance;Initial Catalog=master;TrustServerCertificate=True;" +$Connection = New-Object System.Data.SqlClient.SqlConnection($connectionString) +[System.Security.SecureString]$SQLPwd = $sqlpassword | ConvertTo-SecureString -AsPlainText -Force +$SQLPwd.MakeReadOnly() +$cred = New-Object System.Data.SqlClient.SqlCredential($sqlusername,$SQLPwd) +$Connection.credential = $cred + +try { + $Connection.open() + + $command = New-Object system.Data.SqlClient.SqlCommand($Connection) + $command.Connection = $Connection + $command.CommandTimeout = $QueryTimeout + + $command.CommandText = $ConfigureSql + $result = $command.ExecuteReader() + $table = New-Object System.Data.DataTable + $table.Load($result) +} +catch { + Write-Error "Fehler bei der Ausführung: $_" +} +finally { + if ($null -ne $serverConnection -and $serverConnection.IsOpen) { + $serverConnection.Disconnect() + } +} +try { + $Connection.Close() +} +catch { +} + +Stop-Transcript diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Configure-legacySQL.ps1 b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Configure-legacySQL.ps1 new file mode 100644 index 000000000..b4cccbbde --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Configure-legacySQL.ps1 @@ -0,0 +1,82 @@ +param( + [Parameter(Mandatory)] + [string] $sqlusername, + [Parameter(Mandatory)] + [string] $sqlpassword, + [string]$ServerInstance = 'localhost' +) + +$ErrorActionPreference = 'Stop' + +$logPath = 'C:\Windows\Temp\Configure-legacySQL.log' +Start-Transcript -Path $logPath -Append + +Write-Host 'Configuring SQL legacy Instance...' + +$ConfigureSql = @" +USE master +GO +EXEC sp_configure 'CLR Enabled', 1 +RECONFIGURE WITH OVERRIDE +GO +USE [master] +GO +CREATE MASTER KEY ENCRYPTION BY PASSWORD = '$sqlpassword' +GO +"@ + +[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.ConnectionInfo") | Out-Null +[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo") | Out-Null + +$connectionString = "Data Source=$ServerInstance;Initial Catalog=master;TrustServerCertificate=True;" +$Connection = New-Object System.Data.SqlClient.SqlConnection($connectionString) +[System.Security.SecureString]$SQLPwd = $sqlpassword | ConvertTo-SecureString -AsPlainText -Force +$SQLPwd.MakeReadOnly() +$cred = New-Object System.Data.SqlClient.SqlCredential($sqlusername,$SQLPwd) +$Connection.credential = $cred +#$Connection.open() + +#$command = New-Object system.Data.SqlClient.SqlCommand($Connection) +#$command.Connection = $Connection +#$command.CommandTimeout = $QueryTimeout + +#$command.CommandText = $ConfigureSql +#$result = $command.ExecuteReader() +#$table = New-Object System.Data.DataTable +#$table.Load($result) + +# 5. SMO-Serververbindung initialisieren +$serverConnection = New-Object Microsoft.SqlServer.Management.Common.ServerConnection($Connection) +$server = New-Object Microsoft.SqlServer.Management.Smo.Server($serverConnection) + +try { + # Ausführung des Skripts inkl. GO-Trenner + [void]$server.ConnectionContext.ExecuteNonQuery($ConfigureSql) + #Write-Host "Skript erfolgreich auf SQL MI ausgeführt." -ForegroundColor Green +} +catch { + Write-Error "Fehler bei der Ausführung: $_" +} +finally { + if ($null -ne $serverConnection -and $serverConnection.IsOpen) { + $serverConnection.Disconnect() + } +} +try { + $Connection.Close() +} +catch { +} + +Try { + Write-Host "Installing and configuring Windows Failover Cluster..." -ForegroundColor Green + Install-WindowsFeature -Name Failover-Clustering -IncludeManagementTools -IncludeAllSubFeature + $clus = New-Cluster -Name "CLU01" -AdministrativeAccessPoint None -Verbose -Force + Enable-SqlAlwaysOn -Path SQLSERVER:\SQL\legacysql2016\default -Force + Restart-Service -Name "MSSQLSERVER" -Force +} +Catch { + Write-Host "Error configuring SQL legacy Instance: $_" +} + +Stop-Transcript diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Download-Labs.ps1 b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Download-Labs.ps1 new file mode 100644 index 000000000..76cc45e77 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Download-Labs.ps1 @@ -0,0 +1,150 @@ +[CmdletBinding()] +param +( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $LabsBaseUri, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string] $DownloadDirectory = "C:\MicroHack\Labs", + + [Parameter(Mandatory = $false)] + [switch] $ForceDownload +) + +$ErrorActionPreference = 'Stop' + +$logPath = 'C:\Windows\Temp\Download-Labs.log' +Start-Transcript -Path $logPath -Append + +try +{ + # + # Parse GitHub URL + # + $BaseURL = ($LabsBaseUri -split "//")[1] + Write-Host "Base URL: $BaseURL" + + $company = ($BaseURL -split "/")[1] + $repo = ($BaseURL -split "/")[2] + $branch = ($BaseURL -split "/")[3] + + $folder = $BaseURL -replace "raw.githubusercontent.com/$company/$repo/$branch/", "" + + Write-Host "Company : $company" + Write-Host "Repo : $repo" + Write-Host "Branch : $branch" + Write-Host "Folder : $folder" + Write-Host "Target : $DownloadDirectory" + + # + # Create target directory if required + # + if (-not (Test-Path -LiteralPath $DownloadDirectory)) + { + New-Item ` + -ItemType Directory ` + -Path $DownloadDirectory ` + -Force | Out-Null + } + + function Get-GitHubFolderContent + { + param ( + [AllowEmptyString()] + [string] $RepoFolder, + + [Parameter(Mandatory)] + [string] $LocalFolder + ) + + $apiUrl = "https://api.github.com/repos/$company/$repo/contents/$($RepoFolder)?ref=$branch" + + Write-Host "" + Write-Host "Reading folder: $RepoFolder" + Write-Host $apiUrl + + try + { + $items = Invoke-RestMethod -Uri $apiUrl + } + catch + { + Write-Warning "Failed to enumerate folder: $RepoFolder" + Write-Warning $_.Exception.Message + return + } + + foreach ($item in $items) + { + switch ($item.type) + { + "file" + { + $destination = Join-Path $LocalFolder $item.name + + Write-Host "Downloading $($item.path)" + + if ((Test-Path $destination) -and (-not $ForceDownload)) + { + Write-Host "File exists. Skipping." + continue + } + + Remove-Item ` + -LiteralPath $destination ` + -Force ` + -ErrorAction SilentlyContinue + + $oldProgress = $ProgressPreference + $ProgressPreference = 'SilentlyContinue' + + try + { + Invoke-WebRequest ` + -Uri $item.download_url ` + -OutFile $destination + } + finally + { + $ProgressPreference = $oldProgress + } + } + + "dir" + { + $subFolder = Join-Path $LocalFolder $item.name + + if (-not (Test-Path $subFolder)) + { + New-Item ` + -ItemType Directory ` + -Path $subFolder ` + -Force | Out-Null + } + + Get-GitHubFolderContent ` + -RepoFolder $item.path ` + -LocalFolder $subFolder + } + + default + { + Write-Host "Skipping unsupported item type '$($item.type)' : $($item.path)" + } + } + } + } + + # + # Start recursive download + # + Get-GitHubFolderContent ` + -RepoFolder $folder ` + -LocalFolder $DownloadDirectory +} +finally +{ + Stop-Transcript +} diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Download-Samples.ps1 b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Download-Samples.ps1 new file mode 100644 index 000000000..fba4af896 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Download-Samples.ps1 @@ -0,0 +1,58 @@ +[CmdletBinding()] +param +( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $SamplesBaseUri, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string] $DownloadDirectory = "C:\Samples", + + [Parameter(Mandatory = $false)] + [switch] $ForceDownload +) + +$ErrorActionPreference = 'Stop' + +$logPath = 'C:\Windows\Temp\Download-Samples.log' +Start-Transcript -Path $logPath -Append + +$BaseURL = ($SamplesBaseUri -split "//")[1] + +$company = ($BaseURL -split "/")[1] +$repo = ($BaseURL -split "/")[2] +$branch = ($BaseURL -split "/")[3] +#$folder = ($BaseURL -split "/")[4] +$folder = $BaseURL -replace "raw.githubusercontent.com/$company/$repo/$branch/", "" + +if (-not (Test-Path -LiteralPath $DownloadDirectory)) +{ + New-Item ` + -ItemType Directory ` + -Path $DownloadDirectory ` + -Force | + Out-Null +} + +$apiUrl = "https://api.github.com/repos/$company/$repo/contents/$($folder)?ref=$branch" + +$items = Invoke-RestMethod -Uri $apiUrl + +foreach ($item in $items) { + if ($item.type -eq "file") { + $destination = Join-Path $DownloadDirectory $item.name + Write-Host "Downloading $($item.path)" + if ((Test-Path $destination) -and (-not $ForceDownload.IsPresent)) { + Write-Host "File $($item.name) already exists. Skipping download." + } else { + Remove-Item -LiteralPath $destination -ErrorAction SilentlyContinue -Force + $lastProgressPreference = $ProgressPreference + $ProgressPreference = 'SilentlyContinue' + Invoke-WebRequest -Uri $item.download_url -OutFile $destination + $ProgressPreference = $lastProgressPreference + } + } +} + +Stop-Transcript \ No newline at end of file diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Download-TeamDatabases.ps1 b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Download-TeamDatabases.ps1 new file mode 100644 index 000000000..87ca8d5e2 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Download-TeamDatabases.ps1 @@ -0,0 +1,198 @@ +<# +.SYNOPSIS +Downloads four SQL Server backup files. + +.DESCRIPTION +For each team, the script restores: + + TEAMxx_LocalMasterDataDB + TEAMxx_SharedMasterDataDB + TEAMxx_TenantDataDB + TEAMxx_GlobalDataDB + +The physical database files are relocated to the SQL Server instance's +default data and log directories. Every restored database receives unique +physical file names, including backups containing multiple data or log files. + +The script is designed for the local default SQL Server instance. + +.REQUIREMENTS +- Windows PowerShell 5.1 or PowerShell 7 +- SqlServer PowerShell module +- Local SQL Server default instance +- Permission to create and restore databases +- SQL Server service account must be able to read the download directory + +.EXAMPLE +.\Restore-TeamDatabases.ps1 ` + -TeamCount 4 ` + -BackupBaseUri "https://raw.githubusercontent.com/organisation/repository/main/backups" + +.EXAMPLE +.\Restore-TeamDatabases.ps1 ` + -TeamCount 4 ` + -BackupBaseUri "https://storageaccount.blob.core.windows.net/backups?" + +.NOTES +When the base URI contains a query string, such as a SAS token, the script +inserts the backup file name before the query string. +#> + +[CmdletBinding()] +param +( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $BackupBaseUri, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string] $DownloadDirectory = "C:\SQLBackups\TeamDatabases", + + [Parameter(Mandatory = $false)] + [switch] $ForceDownload +) + +$ErrorActionPreference = 'Stop' + +$logPath = 'C:\Windows\Temp\Dwonload-TeamDatabases.log' +Start-Transcript -Path $logPath -Append + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +# Windows PowerShell 5.1 can otherwise negotiate an older TLS version. +[Net.ServicePointManager]::SecurityProtocol = + [Net.ServicePointManager]::SecurityProtocol -bor + [Net.SecurityProtocolType]::Tls12 + +$backupNames = @( + "LocalMasterDataDB", + "SharedMasterDataDB", + "TenantDataDB", + "GlobalDataDB" +) + +function Get-BackupDownloadUri +{ + param + ( + [Parameter(Mandatory = $true)] + [string] $BaseUri, + + [Parameter(Mandatory = $true)] + [string] $FileName + ) + + # Handles normal URLs: + # https://server/backups + Database.bak + # + # Also handles URLs containing a query string: + # https://server/container?sv=... becomes + # https://server/container/Database.bak?sv=... + + $questionMarkPosition = $BaseUri.IndexOf("?") + + if ($questionMarkPosition -ge 0) + { + $uriPath = $BaseUri.Substring(0, $questionMarkPosition).TrimEnd("/") + $uriQuery = $BaseUri.Substring($questionMarkPosition) + + return "$uriPath/$FileName$uriQuery" + } + + return "$($BaseUri.TrimEnd('/'))/$FileName" +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +if (-not (Test-Path -LiteralPath $DownloadDirectory)) +{ + New-Item ` + -ItemType Directory ` + -Path $DownloadDirectory ` + -Force | + Out-Null +} + +$downloadedBackups = @{} + +foreach ($backupName in $backupNames) +{ + $fileName = "$backupName.bak" + $destination = Join-Path $DownloadDirectory $fileName + $downloadUri = Get-BackupDownloadUri ` + -BaseUri $BackupBaseUri ` + -FileName $fileName + + if ((Test-Path -LiteralPath $destination) -and + -not $ForceDownload.IsPresent) + { + Write-Host "Using existing backup: $destination" + } + else + { + Write-Host "Downloading [$fileName] from $downloadUri ..." + + $temporaryFile = "$destination.download" + + Remove-Item ` + -LiteralPath $temporaryFile ` + -Force ` + -ErrorAction SilentlyContinue + + try + { + $invokeWebRequestParameters = @{ + Uri = $downloadUri + OutFile = $temporaryFile + ErrorAction = "Stop" + } + + # UseBasicParsing exists in Windows PowerShell 5.1 but not in + # newer editions in the same form. + if ($PSVersionTable.PSEdition -eq "Desktop") + { + $invokeWebRequestParameters.UseBasicParsing = $true + } + + $lastProgressPreference = $ProgressPreference + $ProgressPreference = 'SilentlyContinue' + Invoke-WebRequest @invokeWebRequestParameters + $ProgressPreference = $lastProgressPreference + + if (-not (Test-Path -LiteralPath $temporaryFile)) + { + throw "The downloaded file was not created." + } + + if ((Get-Item -LiteralPath $temporaryFile).Length -eq 0) + { + throw "The downloaded file is empty." + } + + Move-Item ` + -LiteralPath $temporaryFile ` + -Destination $destination ` + -Force + } + catch + { + Remove-Item ` + -LiteralPath $temporaryFile ` + -Force ` + -ErrorAction SilentlyContinue + + throw "Download of [$fileName] failed: $($_.Exception.Message)" + } + + Write-Host "Downloaded [$fileName] to [$destination]." ` + -ForegroundColor Green + } + + $downloadedBackups[$backupName] = $destination +} + +Stop-Transcript \ No newline at end of file diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Install-AzureCLI.ps1 b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Install-AzureCLI.ps1 new file mode 100644 index 000000000..52cead37b --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Install-AzureCLI.ps1 @@ -0,0 +1,16 @@ +$ErrorActionPreference = 'Stop' + +$logPath = 'C:\Windows\Temp\Install-AzureCLI.log' +Start-Transcript -Path $logPath -Append + +$ProgressPreference = 'SilentlyContinue' +Invoke-WebRequest -Uri https://aka.ms/installazurecliwindows -OutFile .\AzureCLI.msi +Start-Process msiexec.exe -Wait -ArgumentList '/I', 'AzureCLI.msi', '/quiet' +Remove-Item .\AzureCLI.msi + +$env:PATH = [System.Environment]::GetEnvironmentVariable( + "PATH", + [System.EnvironmentVariableTarget]::Machine +) + +Stop-Transcript \ No newline at end of file diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Restore-SampleDatabases.ps1 b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Restore-SampleDatabases.ps1 new file mode 100644 index 000000000..713569c43 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Restore-SampleDatabases.ps1 @@ -0,0 +1,655 @@ +<# +.SYNOPSIS +Downloads SQL Server sample database WideWorldImports and restores it. + +.DESCRIPTION +The physical database files are relocated to the SQL Server instance's +default data and log directories. Every restored database receives unique +physical file names, including backups containing multiple data or log files. + +The script is designed for the local default SQL Server instance. + +.REQUIREMENTS +- Windows PowerShell 5.1 or PowerShell 7 +- SqlServer PowerShell module +- Local SQL Server default instance +- Permission to create and restore databases +- SQL Server service account must be able to read the download directory + +.EXAMPLE +.\Restore-SampleDatabases.ps1 ` + -BackupUri "https://github.com/Microsoft/sql-server-samples/releases/download/wide-world-importers-v1.0/WideWorldImporters-Full.bak" + +.EXAMPLE +.\Restore-SampleDatabases.ps1 ` + -BackupUri "https://github.com/Microsoft/sql-server-samples/releases/download/wide-world-importers-v1.0/WideWorldImporters-Full.bak" ` + -ReplaceExisting + +.NOTES +When the base URI contains a query string, such as a SAS token, the script +inserts the backup file name before the query string. +#> + +[CmdletBinding()] +param +( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $BackupUri, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string] $DownloadDirectory = "C:\SQLBackups", + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string] $ServerInstance = "localhost", + + [Parameter(Mandatory = $false)] + [string] $sqlusername, + + [Parameter(Mandatory = $false)] + [string] $sqlpassword, + + [Parameter(Mandatory = $false)] + [switch] $ReplaceExisting, + + [Parameter(Mandatory = $false)] + [switch] $ForceDownload +) + +$logPath = 'C:\Windows\Temp\Restore-SampleDatabases.log' +Start-Transcript -Path $logPath -Append + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +# Windows PowerShell 5.1 can otherwise negotiate an older TLS version. +[Net.ServicePointManager]::SecurityProtocol = + [Net.ServicePointManager]::SecurityProtocol -bor + [Net.SecurityProtocolType]::Tls12 + +function Escape-SqlString +{ + param + ( + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [string] $Value + ) + + return $Value.Replace("'", "''") +} + +function Escape-SqlIdentifier +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $Value + ) + + return $Value.Replace("]", "]]") +} + +function Invoke-DatabaseQuery +{ + param + ( + [Parameter(Mandatory = $true)] + [string] $Query, + + [Parameter(Mandatory = $false)] + [int] $QueryTimeout = 0 + ) + + # Invoke-Sqlcmd ` + # -ServerInstance $ServerInstance ` + # -Database "master" ` + # -Query $Query ` + # -TrustServerCertificate ` + # -QueryTimeout $QueryTimeout ` + # -AbortOnError ` + # -ErrorAction Stop + + if (-not [string]::IsNullOrWhiteSpace($sqlusername) -and + -not [string]::IsNullOrWhiteSpace($sqlpassword)) + { + $connectionString = "Data Source=$ServerInstance;Initial Catalog=master;TrustServerCertificate=True;" + $Connection = New-Object System.Data.SqlClient.SqlConnection($connectionString) + [System.Security.SecureString]$SQLPwd = $sqlpassword | ConvertTo-SecureString -AsPlainText -Force + $SQLPwd.MakeReadOnly() + $cred = New-Object System.Data.SqlClient.SqlCredential($sqlusername,$SQLPwd) + $Connection.credential = $cred + } + else + { + $connectionString = "Data Source=$ServerInstance;Initial Catalog=master;Integrated Security=True;TrustServerCertificate=True;" + $Connection = New-Object System.Data.SqlClient.SqlConnection($connectionString) + } + $Connection.open() + + $command = New-Object system.Data.SqlClient.SqlCommand($Connection) + $command.Connection = $Connection + $command.CommandTimeout = $QueryTimeout + + $command.CommandText = $Query # "SELECT 1 FROM sys.databases WHERE name = '$Databasename'" + + $result = $command.ExecuteReader() + $table = New-Object System.Data.DataTable + $table.Load($result) + + $Connection.Close() + $table + +} + +function Get-SqlDefaultDirectories +{ + $query = @" +SELECT + CONVERT(nvarchar(4000), SERVERPROPERTY('InstanceDefaultDataPath')) + AS DataPath, + CONVERT(nvarchar(4000), SERVERPROPERTY('InstanceDefaultLogPath')) + AS LogPath, + ( + SELECT TOP (1) + LEFT( + physical_name, + LEN(physical_name) - + CHARINDEX('\', REVERSE(physical_name)) + 1 + ) + FROM master.sys.database_files + WHERE type = 0 + ORDER BY file_id + ) AS FallbackDataPath, + ( + SELECT TOP (1) + LEFT( + physical_name, + LEN(physical_name) - + CHARINDEX('\', REVERSE(physical_name)) + 1 + ) + FROM master.sys.database_files + WHERE type = 1 + ORDER BY file_id + ) AS FallbackLogPath; +"@ + + $result = Invoke-DatabaseQuery -Query $query | + Select-Object -First 1 + + $dataPath = [string] $result.DataPath + $logPath = [string] $result.LogPath + + if ([string]::IsNullOrWhiteSpace($dataPath)) + { + $dataPath = [string] $result.FallbackDataPath + } + + if ([string]::IsNullOrWhiteSpace($logPath)) + { + $logPath = [string] $result.FallbackLogPath + } + + if ([string]::IsNullOrWhiteSpace($dataPath)) + { + throw "The default SQL Server data directory could not be determined." + } + + if ([string]::IsNullOrWhiteSpace($logPath)) + { + throw "The default SQL Server log directory could not be determined." + } + + [PSCustomObject] @{ + DataPath = $dataPath.TrimEnd('\') + LogPath = $logPath.TrimEnd('\') + } +} + +function Get-BackupFileList +{ + param + ( + [Parameter(Mandatory = $true)] + [string] $BackupFile + ) + + $escapedBackupFile = Escape-SqlString -Value $BackupFile + + $fileList = @( + Invoke-DatabaseQuery -Query @" +RESTORE FILELISTONLY +FROM DISK = N'$escapedBackupFile'; +"@ + ) + + if ($fileList.Count -eq 0) + { + throw "The backup does not contain any database files: $BackupFile" + } + + return $fileList +} + +function Get-TargetFileExtension +{ + param + ( + [Parameter(Mandatory = $true)] + [object] $BackupFileInformation, + + [Parameter(Mandatory = $true)] + [int] $FileNumberForType + ) + + $originalExtension = [System.IO.Path]::GetExtension( + [string] $BackupFileInformation.PhysicalName + ) + + if (-not [string]::IsNullOrWhiteSpace($originalExtension)) + { + return $originalExtension + } + + if ([string] $BackupFileInformation.Type -eq "L") + { + return ".ldf" + } + + if ($FileNumberForType -eq 1) + { + return ".mdf" + } + + return ".ndf" +} + +function New-RestoreMoveClauses +{ + param + ( + [Parameter(Mandatory = $true)] + [string] $DatabaseName, + + [Parameter(Mandatory = $true)] + [object[]] $BackupFileList, + + [Parameter(Mandatory = $true)] + [string] $DefaultDataPath, + + [Parameter(Mandatory = $true)] + [string] $DefaultLogPath + ) + + $moveClauses = @() + $dataFileNumber = 0 + $logFileNumber = 0 + + foreach ($backupFileInformation in $BackupFileList) + { + $logicalName = [string] $backupFileInformation.LogicalName + $fileType = [string] $backupFileInformation.Type + + if ([string]::IsNullOrWhiteSpace($logicalName)) + { + throw "A backup file entry has no logical file name." + } + + switch ($fileType) + { + "D" + { + $dataFileNumber++ + + $extension = Get-TargetFileExtension ` + -BackupFileInformation $backupFileInformation ` + -FileNumberForType $dataFileNumber + + if ($dataFileNumber -eq 1) + { + $targetFileName = "$DatabaseName$extension" + } + else + { + $targetFileName = "{0}_Data{1:D2}{2}" -f ` + $DatabaseName, + $dataFileNumber, + $extension + } + + $targetFilePath = Join-Path ` + -Path $DefaultDataPath ` + -ChildPath $targetFileName + } + + "S" + { + $dataFileNumber++ + + $extension = Get-TargetFileExtension ` + -BackupFileInformation $backupFileInformation ` + -FileNumberForType $dataFileNumber + + if ($dataFileNumber -eq 1) + { + $targetFileName = "$DatabaseName$extension" + } + else + { + $targetFileName = "{0}_Data{1:D2}{2}" -f ` + $DatabaseName, + $dataFileNumber, + $extension + } + + $targetFilePath = Join-Path ` + -Path $DefaultDataPath ` + -ChildPath $targetFileName + } + + "L" + { + $logFileNumber++ + + $extension = Get-TargetFileExtension ` + -BackupFileInformation $backupFileInformation ` + -FileNumberForType $logFileNumber + + if ($logFileNumber -eq 1) + { + $targetFileName = "${DatabaseName}_log$extension" + } + else + { + $targetFileName = "{0}_Log{1:D2}{2}" -f ` + $DatabaseName, + $logFileNumber, + $extension + } + + $targetFilePath = Join-Path ` + -Path $DefaultLogPath ` + -ChildPath $targetFileName + } + + default + { + throw @" +The backup contains unsupported file type '$fileType' for logical file +'$logicalName'. The script currently supports normal data files (D) and +transaction log files (L). +"@ + } + } + + $escapedLogicalName = Escape-SqlString -Value $logicalName + $escapedTargetPath = Escape-SqlString -Value $targetFilePath + + $moveClauses += "MOVE N'$escapedLogicalName' TO N'$escapedTargetPath'" + } + + return $moveClauses +} + +function Restore-SampleDatabase +{ + param + ( + [Parameter(Mandatory = $true)] + [string] $DatabaseName, + + [Parameter(Mandatory = $true)] + [string] $BackupFile, + + [Parameter(Mandatory = $true)] + [string] $DefaultDataPath, + + [Parameter(Mandatory = $true)] + [string] $DefaultLogPath + ) + + $escapedDatabaseName = Escape-SqlIdentifier -Value $DatabaseName + $escapedDatabaseLiteral = Escape-SqlString -Value $DatabaseName + $escapedBackupFile = Escape-SqlString -Value $BackupFile + + $databaseExistsQuery = @" +SELECT + CASE + WHEN DB_ID(N'$escapedDatabaseLiteral') IS NULL THEN 0 + ELSE 1 + END AS DatabaseExists; +"@ + + $databaseExists = [int] ( + Invoke-DatabaseQuery -Query $databaseExistsQuery | + Select-Object -First 1 + ).DatabaseExists + + if ($databaseExists -eq 1 -and -not $ReplaceExisting.IsPresent) + { + Write-Warning @" +Database [$DatabaseName] already exists and will be skipped. +Use -ReplaceExisting if the database should be overwritten. +"@ + return + } + + $backupFileList = @( + Get-BackupFileList -BackupFile $BackupFile + ) + + $moveClauses = @( + New-RestoreMoveClauses ` + -DatabaseName $DatabaseName ` + -BackupFileList $backupFileList ` + -DefaultDataPath $DefaultDataPath ` + -DefaultLogPath $DefaultLogPath + ) + + if ($moveClauses.Count -eq 0) + { + throw "No MOVE clauses were generated for [$DatabaseName]." + } + + $restoreOptions = @() + + foreach ($moveClause in $moveClauses) + { + $restoreOptions += $moveClause + } + + $restoreOptions += "RECOVERY" + $restoreOptions += "STATS = 5" + + if ($ReplaceExisting.IsPresent) + { + $restoreOptions += "REPLACE" + } + + $restoreOptionText = $restoreOptions -join ",`r`n " + + $prepareExistingDatabase = "" + + if ($databaseExists -eq 1) + { + $prepareExistingDatabase = @" +ALTER DATABASE [$escapedDatabaseName] + SET SINGLE_USER + WITH ROLLBACK IMMEDIATE; +"@ + } + + $restoreQuery = @" +$prepareExistingDatabase + +RESTORE DATABASE [$escapedDatabaseName] +FROM DISK = N'$escapedBackupFile' +WITH + $restoreOptionText; + +ALTER DATABASE [$escapedDatabaseName] + SET MULTI_USER; +"@ + + Write-Host "" + Write-Host "Restoring [$DatabaseName]..." + Write-Host "" + + try + { + Invoke-DatabaseQuery ` + -Query $restoreQuery ` + -QueryTimeout 0 | + Out-Host + + Write-Host "Restored [$DatabaseName]." -ForegroundColor Green + } + catch + { + if ($databaseExists -eq 1) + { + try + { + Invoke-DatabaseQuery -Query @" +IF DB_ID(N'$escapedDatabaseLiteral') IS NOT NULL +BEGIN + ALTER DATABASE [$escapedDatabaseName] + SET MULTI_USER; +END; +"@ + } + catch + { + Write-Warning "Could not return [$DatabaseName] to MULTI_USER after a failed restore." + } + } + + throw + } +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +if (-not (Test-Path -LiteralPath $DownloadDirectory)) +{ + New-Item ` + -ItemType Directory ` + -Path $DownloadDirectory ` + -Force | + Out-Null +} + +Write-Host "Testing SQL Server connection to [$ServerInstance]..." + +$serverInformation = Invoke-DatabaseQuery -Query @" +SELECT + CONVERT(nvarchar(128), SERVERPROPERTY('ServerName')) AS ServerName, + CONVERT(nvarchar(128), SERVERPROPERTY('ProductVersion')) AS ProductVersion, + CONVERT(nvarchar(128), SERVERPROPERTY('Edition')) AS Edition; +"@ | Select-Object -First 1 + +Write-Host ( + "Connected to [{0}], SQL Server {1}, {2}." -f + $serverInformation.ServerName, + $serverInformation.ProductVersion, + $serverInformation.Edition +) -ForegroundColor Green + +$defaultDirectories = Get-SqlDefaultDirectories + +Write-Host "Default data path: $($defaultDirectories.DataPath)" +Write-Host "Default log path: $($defaultDirectories.LogPath)" + +$resp = Invoke-WebRequest $BackupUri -Method Head -UseBasicParsing +$filenamestring = @($resp.RawContent.Split() | Where-Object { $_ -match "filename=" })[0] +$filename = $filenamestring.Split("=")[1] + +$destination = Join-Path $DownloadDirectory $fileName + +if ((Test-Path -LiteralPath $destination) -and + -not $ForceDownload.IsPresent) +{ + Write-Host "Using existing backup: $destination" +} +else +{ + Write-Host "Downloading $BackupUri ..." + + $temporaryFile = "$destination.download" + + Remove-Item ` + -LiteralPath $temporaryFile ` + -Force ` + -ErrorAction SilentlyContinue + + try + { + $invokeWebRequestParameters = @{ + Uri = $BackupUri + OutFile = $temporaryFile + ErrorAction = "Stop" + } + + # UseBasicParsing exists in Windows PowerShell 5.1 but not in + # newer editions in the same form. + if ($PSVersionTable.PSEdition -eq "Desktop") + { + $invokeWebRequestParameters.UseBasicParsing = $true + } + + $lastProgressPreference = $ProgressPreference + $ProgressPreference = 'SilentlyContinue' + Invoke-WebRequest @invokeWebRequestParameters + $ProgressPreference = $lastProgressPreference + + if (-not (Test-Path -LiteralPath $temporaryFile)) + { + throw "The downloaded file was not created." + } + + if ((Get-Item -LiteralPath $temporaryFile).Length -eq 0) + { + throw "The downloaded file is empty." + } + + Move-Item ` + -LiteralPath $temporaryFile ` + -Destination $destination ` + -Force + } + catch + { + Remove-Item ` + -LiteralPath $temporaryFile ` + -Force ` + -ErrorAction SilentlyContinue + + throw "Download of [$fileName] failed: $($_.Exception.Message)" + } + + Write-Host "Downloaded [$fileName] to [$destination]." ` + -ForegroundColor Green +} + +$localBackupFile = $destination +$BackupHeader = Invoke-DatabaseQuery -Query "RESTORE HEADERONLY FROM DISK = '$localBackupFile'" +$localDatabaseName = $BackupHeader.DatabaseName + +Write-Host "" +Write-Host "Starting database restore." -ForegroundColor Cyan + +Restore-SampleDatabase ` + -DatabaseName $localDatabaseName ` + -BackupFile $localBackupFile ` + -DefaultDataPath $defaultDirectories.DataPath ` + -DefaultLogPath $defaultDirectories.LogPath + +Write-Host "" +Write-Host "Completed." -ForegroundColor Green + +Stop-Transcript \ No newline at end of file diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Restore-TeamDatabases.ps1 b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Restore-TeamDatabases.ps1 new file mode 100644 index 000000000..569e8bf5e --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Restore-TeamDatabases.ps1 @@ -0,0 +1,720 @@ +<# +.SYNOPSIS +Downloads four SQL Server backup files and restores one database set per team. + +.DESCRIPTION +For each team, the script restores: + + TEAMxx_LocalMasterDataDB + TEAMxx_SharedMasterDataDB + TEAMxx_TenantDataDB + TEAMxx_GlobalDataDB + +The physical database files are relocated to the SQL Server instance's +default data and log directories. Every restored database receives unique +physical file names, including backups containing multiple data or log files. + +The script is designed for the local default SQL Server instance. + +.REQUIREMENTS +- Windows PowerShell 5.1 or PowerShell 7 +- SqlServer PowerShell module +- Local SQL Server default instance +- Permission to create and restore databases +- SQL Server service account must be able to read the download directory + +.EXAMPLE +.\Restore-TeamDatabases.ps1 ` + -TeamCount 4 ` + -BackupBaseUri "https://raw.githubusercontent.com/organisation/repository/main/backups" + +.EXAMPLE +.\Restore-TeamDatabases.ps1 ` + -TeamCount 4 ` + -BackupBaseUri "https://storageaccount.blob.core.windows.net/backups?" ` + -ReplaceExisting + +.NOTES +When the base URI contains a query string, such as a SAS token, the script +inserts the backup file name before the query string. +#> + +[CmdletBinding()] +param +( + [Parameter(Mandatory = $true)] + [string]$TeamName = 'TEAM01', + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $BackupBaseUri, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string] $DownloadDirectory = "C:\SQLBackups\TeamDatabases", + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string] $ServerInstance = "localhost", + + [Parameter(Mandatory = $false)] + [string] $sqlusername, + + [Parameter(Mandatory = $false)] + [string] $sqlpassword, + + [Parameter(Mandatory = $false)] + [switch] $ReplaceExisting, + + [Parameter(Mandatory = $false)] + [switch] $ForceDownload +) + +$logPath = 'C:\Windows\Temp\Restore-TeamDatabases.log' +Start-Transcript -Path $logPath -Append + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +# Windows PowerShell 5.1 can otherwise negotiate an older TLS version. +[Net.ServicePointManager]::SecurityProtocol = + [Net.ServicePointManager]::SecurityProtocol -bor + [Net.SecurityProtocolType]::Tls12 + +$backupNames = @( + "LocalMasterDataDB", + "SharedMasterDataDB", + "TenantDataDB", + "GlobalDataDB" +) + +function Escape-SqlString +{ + param + ( + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [string] $Value + ) + + return $Value.Replace("'", "''") +} + +function Escape-SqlIdentifier +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $Value + ) + + return $Value.Replace("]", "]]") +} + +function Get-BackupDownloadUri +{ + param + ( + [Parameter(Mandatory = $true)] + [string] $BaseUri, + + [Parameter(Mandatory = $true)] + [string] $FileName + ) + + # Handles normal URLs: + # https://server/backups + Database.bak + # + # Also handles URLs containing a query string: + # https://server/container?sv=... becomes + # https://server/container/Database.bak?sv=... + + $questionMarkPosition = $BaseUri.IndexOf("?") + + if ($questionMarkPosition -ge 0) + { + $uriPath = $BaseUri.Substring(0, $questionMarkPosition).TrimEnd("/") + $uriQuery = $BaseUri.Substring($questionMarkPosition) + + return "$uriPath/$FileName$uriQuery" + } + + return "$($BaseUri.TrimEnd('/'))/$FileName" +} + +function Invoke-DatabaseQuery +{ + param + ( + [Parameter(Mandatory = $true)] + [string] $Query, + + [Parameter(Mandatory = $false)] + [int] $QueryTimeout = 0 + ) + + # Invoke-Sqlcmd ` + # -ServerInstance $ServerInstance ` + # -Database "master" ` + # -Query $Query ` + # -TrustServerCertificate ` + # -QueryTimeout $QueryTimeout ` + # -AbortOnError ` + # -ErrorAction Stop + + if (-not [string]::IsNullOrWhiteSpace($sqlusername) -and + -not [string]::IsNullOrWhiteSpace($sqlpassword)) + { + $connectionString = "Data Source=$ServerInstance;Initial Catalog=master;TrustServerCertificate=True;" + $Connection = New-Object System.Data.SqlClient.SqlConnection($connectionString) + [System.Security.SecureString]$SQLPwd = $sqlpassword | ConvertTo-SecureString -AsPlainText -Force + $SQLPwd.MakeReadOnly() + $cred = New-Object System.Data.SqlClient.SqlCredential($sqlusername,$SQLPwd) + $Connection.credential = $cred + } + else + { + $connectionString = "Data Source=$ServerInstance;Initial Catalog=master;Integrated Security=True;TrustServerCertificate=True;" + $Connection = New-Object System.Data.SqlClient.SqlConnection($connectionString) + } + $Connection.open() + + $command = New-Object system.Data.SqlClient.SqlCommand($Connection) + $command.Connection = $Connection + $command.CommandTimeout = $QueryTimeout + + $command.CommandText = $Query # "SELECT 1 FROM sys.databases WHERE name = '$Databasename'" + + $result = $command.ExecuteReader() + $table = New-Object System.Data.DataTable + $table.Load($result) + + $Connection.Close() + $table + +} + +function Get-SqlDefaultDirectories +{ + $query = @" +SELECT + CONVERT(nvarchar(4000), SERVERPROPERTY('InstanceDefaultDataPath')) + AS DataPath, + CONVERT(nvarchar(4000), SERVERPROPERTY('InstanceDefaultLogPath')) + AS LogPath, + ( + SELECT TOP (1) + LEFT( + physical_name, + LEN(physical_name) - + CHARINDEX('\', REVERSE(physical_name)) + 1 + ) + FROM master.sys.database_files + WHERE type = 0 + ORDER BY file_id + ) AS FallbackDataPath, + ( + SELECT TOP (1) + LEFT( + physical_name, + LEN(physical_name) - + CHARINDEX('\', REVERSE(physical_name)) + 1 + ) + FROM master.sys.database_files + WHERE type = 1 + ORDER BY file_id + ) AS FallbackLogPath; +"@ + + $result = Invoke-DatabaseQuery -Query $query | + Select-Object -First 1 + + $dataPath = [string] $result.DataPath + $logPath = [string] $result.LogPath + + if ([string]::IsNullOrWhiteSpace($dataPath)) + { + $dataPath = [string] $result.FallbackDataPath + } + + if ([string]::IsNullOrWhiteSpace($logPath)) + { + $logPath = [string] $result.FallbackLogPath + } + + if ([string]::IsNullOrWhiteSpace($dataPath)) + { + throw "The default SQL Server data directory could not be determined." + } + + if ([string]::IsNullOrWhiteSpace($logPath)) + { + throw "The default SQL Server log directory could not be determined." + } + + [PSCustomObject] @{ + DataPath = $dataPath.TrimEnd('\') + LogPath = $logPath.TrimEnd('\') + } +} + +function Get-BackupFileList +{ + param + ( + [Parameter(Mandatory = $true)] + [string] $BackupFile + ) + + $escapedBackupFile = Escape-SqlString -Value $BackupFile + + $fileList = @( + Invoke-DatabaseQuery -Query @" +RESTORE FILELISTONLY +FROM DISK = N'$escapedBackupFile'; +"@ + ) + + if ($fileList.Count -eq 0) + { + throw "The backup does not contain any database files: $BackupFile" + } + + return $fileList +} + +function Get-TargetFileExtension +{ + param + ( + [Parameter(Mandatory = $true)] + [object] $BackupFileInformation, + + [Parameter(Mandatory = $true)] + [int] $FileNumberForType + ) + + $originalExtension = [System.IO.Path]::GetExtension( + [string] $BackupFileInformation.PhysicalName + ) + + if (-not [string]::IsNullOrWhiteSpace($originalExtension)) + { + return $originalExtension + } + + if ([string] $BackupFileInformation.Type -eq "L") + { + return ".ldf" + } + + if ($FileNumberForType -eq 1) + { + return ".mdf" + } + + return ".ndf" +} + +function New-RestoreMoveClauses +{ + param + ( + [Parameter(Mandatory = $true)] + [string] $DatabaseName, + + [Parameter(Mandatory = $true)] + [object[]] $BackupFileList, + + [Parameter(Mandatory = $true)] + [string] $DefaultDataPath, + + [Parameter(Mandatory = $true)] + [string] $DefaultLogPath + ) + + $moveClauses = @() + $dataFileNumber = 0 + $logFileNumber = 0 + + foreach ($backupFileInformation in $BackupFileList) + { + $logicalName = [string] $backupFileInformation.LogicalName + $fileType = [string] $backupFileInformation.Type + + if ([string]::IsNullOrWhiteSpace($logicalName)) + { + throw "A backup file entry has no logical file name." + } + + switch ($fileType) + { + "D" + { + $dataFileNumber++ + + $extension = Get-TargetFileExtension ` + -BackupFileInformation $backupFileInformation ` + -FileNumberForType $dataFileNumber + + if ($dataFileNumber -eq 1) + { + $targetFileName = "$DatabaseName$extension" + } + else + { + $targetFileName = "{0}_Data{1:D2}{2}" -f ` + $DatabaseName, + $dataFileNumber, + $extension + } + + $targetFilePath = Join-Path ` + -Path $DefaultDataPath ` + -ChildPath $targetFileName + } + + "S" + { + $dataFileNumber++ + + $extension = Get-TargetFileExtension ` + -BackupFileInformation $backupFileInformation ` + -FileNumberForType $dataFileNumber + + if ($dataFileNumber -eq 1) + { + $targetFileName = "$DatabaseName$extension" + } + else + { + $targetFileName = "{0}_Data{1:D2}{2}" -f ` + $DatabaseName, + $dataFileNumber, + $extension + } + + $targetFilePath = Join-Path ` + -Path $DefaultDataPath ` + -ChildPath $targetFileName + } + + "L" + { + $logFileNumber++ + + $extension = Get-TargetFileExtension ` + -BackupFileInformation $backupFileInformation ` + -FileNumberForType $logFileNumber + + if ($logFileNumber -eq 1) + { + $targetFileName = "${DatabaseName}_log$extension" + } + else + { + $targetFileName = "{0}_Log{1:D2}{2}" -f ` + $DatabaseName, + $logFileNumber, + $extension + } + + $targetFilePath = Join-Path ` + -Path $DefaultLogPath ` + -ChildPath $targetFileName + } + + default + { + throw @" +The backup contains unsupported file type '$fileType' for logical file +'$logicalName'. The script currently supports normal data files (D) and +transaction log files (L). +"@ + } + } + + $escapedLogicalName = Escape-SqlString -Value $logicalName + $escapedTargetPath = Escape-SqlString -Value $targetFilePath + + $moveClauses += "MOVE N'$escapedLogicalName' TO N'$escapedTargetPath'" + } + + return $moveClauses +} + +function Restore-TeamDatabase +{ + param + ( + [Parameter(Mandatory = $true)] + [string] $DatabaseName, + + [Parameter(Mandatory = $true)] + [string] $BackupFile, + + [Parameter(Mandatory = $true)] + [string] $DefaultDataPath, + + [Parameter(Mandatory = $true)] + [string] $DefaultLogPath + ) + + $escapedDatabaseName = Escape-SqlIdentifier -Value $DatabaseName + $escapedDatabaseLiteral = Escape-SqlString -Value $DatabaseName + $escapedBackupFile = Escape-SqlString -Value $BackupFile + + $databaseExistsQuery = @" +SELECT + CASE + WHEN DB_ID(N'$escapedDatabaseLiteral') IS NULL THEN 0 + ELSE 1 + END AS DatabaseExists; +"@ + + $databaseExists = [int] ( + Invoke-DatabaseQuery -Query $databaseExistsQuery | + Select-Object -First 1 + ).DatabaseExists + + if ($databaseExists -eq 1 -and -not $ReplaceExisting.IsPresent) + { + Write-Warning @" +Database [$DatabaseName] already exists and will be skipped. +Use -ReplaceExisting if the database should be overwritten. +"@ + return + } + + $backupFileList = @( + Get-BackupFileList -BackupFile $BackupFile + ) + + $moveClauses = @( + New-RestoreMoveClauses ` + -DatabaseName $DatabaseName ` + -BackupFileList $backupFileList ` + -DefaultDataPath $DefaultDataPath ` + -DefaultLogPath $DefaultLogPath + ) + + if ($moveClauses.Count -eq 0) + { + throw "No MOVE clauses were generated for [$DatabaseName]." + } + + $restoreOptions = @() + + foreach ($moveClause in $moveClauses) + { + $restoreOptions += $moveClause + } + + $restoreOptions += "RECOVERY" + $restoreOptions += "STATS = 5" + + if ($ReplaceExisting.IsPresent) + { + $restoreOptions += "REPLACE" + } + + $restoreOptionText = $restoreOptions -join ",`r`n " + + $prepareExistingDatabase = "" + + if ($databaseExists -eq 1) + { + $prepareExistingDatabase = @" +ALTER DATABASE [$escapedDatabaseName] + SET SINGLE_USER + WITH ROLLBACK IMMEDIATE; +"@ + } + + $restoreQuery = @" +$prepareExistingDatabase + +RESTORE DATABASE [$escapedDatabaseName] +FROM DISK = N'$escapedBackupFile' +WITH + $restoreOptionText; + +ALTER DATABASE [$escapedDatabaseName] + SET MULTI_USER; +"@ + + Write-Host "" + Write-Host "Restoring [$DatabaseName]..." + Write-Host "" + + try + { + Invoke-DatabaseQuery ` + -Query $restoreQuery ` + -QueryTimeout 0 | + Out-Host + + Write-Host "Restored [$DatabaseName]." -ForegroundColor Green + } + catch + { + if ($databaseExists -eq 1) + { + try + { + Invoke-DatabaseQuery -Query @" +IF DB_ID(N'$escapedDatabaseLiteral') IS NOT NULL +BEGIN + ALTER DATABASE [$escapedDatabaseName] + SET MULTI_USER; +END; +"@ + } + catch + { + Write-Warning "Could not return [$DatabaseName] to MULTI_USER after a failed restore." + } + } + + throw + } +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +if (-not (Test-Path -LiteralPath $DownloadDirectory)) +{ + New-Item ` + -ItemType Directory ` + -Path $DownloadDirectory ` + -Force | + Out-Null +} + +Write-Host "Testing SQL Server connection to [$ServerInstance]..." + +$serverInformation = Invoke-DatabaseQuery -Query @" +SELECT + CONVERT(nvarchar(128), SERVERPROPERTY('ServerName')) AS ServerName, + CONVERT(nvarchar(128), SERVERPROPERTY('ProductVersion')) AS ProductVersion, + CONVERT(nvarchar(128), SERVERPROPERTY('Edition')) AS Edition; +"@ | Select-Object -First 1 + +Write-Host ( + "Connected to [{0}], SQL Server {1}, {2}." -f + $serverInformation.ServerName, + $serverInformation.ProductVersion, + $serverInformation.Edition +) -ForegroundColor Green + +$defaultDirectories = Get-SqlDefaultDirectories + +Write-Host "Default data path: $($defaultDirectories.DataPath)" +Write-Host "Default log path: $($defaultDirectories.LogPath)" + +$downloadedBackups = @{} + +foreach ($backupName in $backupNames) +{ + $fileName = "$backupName.bak" + $destination = Join-Path $DownloadDirectory $fileName + $downloadUri = Get-BackupDownloadUri ` + -BaseUri $BackupBaseUri ` + -FileName $fileName + + if ((Test-Path -LiteralPath $destination) -and + -not $ForceDownload.IsPresent) + { + Write-Host "Using existing backup: $destination" + } + else + { + Write-Host "Downloading [$fileName] from $downloadUri ..." + + $temporaryFile = "$destination.download" + + Remove-Item ` + -LiteralPath $temporaryFile ` + -Force ` + -ErrorAction SilentlyContinue + + try + { + $invokeWebRequestParameters = @{ + Uri = $downloadUri + OutFile = $temporaryFile + ErrorAction = "Stop" + } + + # UseBasicParsing exists in Windows PowerShell 5.1 but not in + # newer editions in the same form. + if ($PSVersionTable.PSEdition -eq "Desktop") + { + $invokeWebRequestParameters.UseBasicParsing = $true + } + + $lastProgressPreference = $ProgressPreference + $ProgressPreference = 'SilentlyContinue' + Invoke-WebRequest @invokeWebRequestParameters + $ProgressPreference = $lastProgressPreference + + if (-not (Test-Path -LiteralPath $temporaryFile)) + { + throw "The downloaded file was not created." + } + + if ((Get-Item -LiteralPath $temporaryFile).Length -eq 0) + { + throw "The downloaded file is empty." + } + + Move-Item ` + -LiteralPath $temporaryFile ` + -Destination $destination ` + -Force + } + catch + { + Remove-Item ` + -LiteralPath $temporaryFile ` + -Force ` + -ErrorAction SilentlyContinue + + throw "Download of [$fileName] failed: $($_.Exception.Message)" + } + + Write-Host "Downloaded [$fileName] to [$destination]." ` + -ForegroundColor Green + } + + $downloadedBackups[$backupName] = $destination +} + +Write-Host "" +Write-Host ( + "Starting database restores team {0}." -f + $TeamName +) -ForegroundColor Cyan + +$teamPrefix = $TeamName + +foreach ($backupName in $backupNames) +{ + $targetDatabaseName = "${teamPrefix}_${backupName}" + + Restore-TeamDatabase ` + -DatabaseName $targetDatabaseName ` + -BackupFile $downloadedBackups[$backupName] ` + -DefaultDataPath $defaultDirectories.DataPath ` + -DefaultLogPath $defaultDirectories.LogPath +} + +Write-Host "" +Write-Host ( + "Completed. Team database for team {0} were processed." -f $TeamName +) -ForegroundColor Green + +Stop-Transcript \ No newline at end of file diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Restore-TeamDatabasesMI.ps1 b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Restore-TeamDatabasesMI.ps1 new file mode 100644 index 000000000..730194fcb --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Restore-TeamDatabasesMI.ps1 @@ -0,0 +1,201 @@ +param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $BackupBaseUri, + + [Parameter(Mandatory = $true)] + [string]$StorageAccountName, + + [Parameter(Mandatory)] + [string]$ManagedInstanceServer, + + [Parameter(Mandatory)] + [string] $sqlusername, + + [Parameter(Mandatory)] + [string] $sqlpassword, + + [string]$ContainerName = 'backups' +) + + +$logPath = 'C:\Windows\Temp\Restore-TeamDatabasesMI.log' +Start-Transcript -Path $logPath -Append + +$ErrorActionPreference = 'Stop' + +Write-Host 'Checking Storage RBAC propagation...' + +for($i=1; $i -le 30; $i++) +{ + try + { + az storage blob list ` + --account-name $StorageAccountName ` + --container-name $ContainerName ` + --auth-mode login ` + --output none + + Write-Host "RBAC active." + break + } + catch + { + Start-Sleep -Seconds 20 + } +} + +Write-Host 'Authenticating with VM Managed Identity...' + +az login --identity --allow-no-subscriptions | Out-Null + +$BackupFiles = @( + "LocalMasterDataDB.bak" + "SharedMasterDataDB.bak" + "TenantDataDB.bak" + "GlobalDataDB.bak" + "TenantCRM.bak" +) + +$CredentialUrl = "https://$StorageAccountName.blob.core.windows.net/$ContainerName" + +# +# Create MI Credential +# +$CredentialSql = @" +IF NOT EXISTS +( + SELECT 1 + FROM sys.credentials + WHERE name = '$CredentialUrl' +) +BEGIN + CREATE CREDENTIAL [$CredentialUrl] + WITH IDENTITY = 'Managed Identity'; +END +"@ + +# Invoke-Sqlcmd ` +# -ServerInstance $ManagedInstanceServer ` +# -Database master ` +# -Username $SqlUser ` +# -Password $SqlPassword ` +# -Encrypt Mandatory ` +# -TrustServerCertificate ` +# -Query $CredentialSql + +$connectionString = "Data Source=$ManagedInstanceServer;Initial Catalog=master;TrustServerCertificate=True;" +$Connection = New-Object System.Data.SqlClient.SqlConnection($connectionString) +[System.Security.SecureString]$SQLPwd = $sqlpassword | ConvertTo-SecureString -AsPlainText -Force +$SQLPwd.MakeReadOnly() +$cred = New-Object System.Data.SqlClient.SqlCredential($sqlusername,$SQLPwd) +$Connection.credential = $cred +$Connection.open() + +$command = New-Object system.Data.SqlClient.SqlCommand($Connection) +$command.Connection = $Connection +$command.CommandTimeout = $QueryTimeout + +$command.CommandText = $CredentialSql + +$result = $command.ExecuteReader() +$table = New-Object System.Data.DataTable +$table.Load($result) + +$Connection.Close() +#$table + +foreach ($BackupFile in $BackupFiles) +{ + $DatabaseName = [System.IO.Path]::GetFileNameWithoutExtension($BackupFile) + + $GithubUrl = + "$BackupBaseUri/$BackupFile" + + $LocalFile = + Join-Path $env:TEMP $BackupFile + + $BlobUrl = + "https://$StorageAccountName.blob.core.windows.net/$ContainerName/$BackupFile" + + Write-Host "Downloading $BackupFile" + + $lastProgressPreference = $ProgressPreference + $ProgressPreference = 'SilentlyContinue' + + Invoke-WebRequest ` + -Uri $GithubUrl ` + -OutFile $LocalFile + + $ProgressPreference = $lastProgressPreference + + Write-Host "Uploading $BackupFile" + + az storage blob upload ` + --auth-mode login ` + --account-name $StorageAccountName ` + --container-name $ContainerName ` + --name $BackupFile ` + --file $LocalFile ` + --overwrite true + + if ($LASTEXITCODE -ne 0) + { + throw "Upload failed for $BackupFile" + } + + $RestoreSql = @" +IF DB_ID('$DatabaseName') IS NULL +BEGIN + + RESTORE DATABASE [$DatabaseName] + FROM URL = '$BlobUrl' + +END +ELSE +BEGIN + + PRINT 'Database [$DatabaseName] already exists. Skipping restore.' + +END +"@ + + Write-Host "Restoring $DatabaseName" + + # Invoke-Sqlcmd ` + # -ServerInstance $ManagedInstanceServer ` + # -Database master ` + # -Username $SqlUser ` + # -Password $SqlPassword ` + # -Encrypt Mandatory ` + # -TrustServerCertificate ` + # -QueryTimeout 0 ` + # -Query $RestoreSql + + $connectionString = "Data Source=$ManagedInstanceServer;Initial Catalog=master;TrustServerCertificate=True;" + $Connection = New-Object System.Data.SqlClient.SqlConnection($connectionString) + [System.Security.SecureString]$SQLPwd = $sqlpassword | ConvertTo-SecureString -AsPlainText -Force + $SQLPwd.MakeReadOnly() + $cred = New-Object System.Data.SqlClient.SqlCredential($sqlusername,$SQLPwd) + $Connection.credential = $cred + $Connection.open() + + $command = New-Object system.Data.SqlClient.SqlCommand($Connection) + $command.Connection = $Connection + $command.CommandTimeout = $QueryTimeout + + $command.CommandText = $RestoreSql + + $result = $command.ExecuteReader() + $table = New-Object System.Data.DataTable + $table.Load($result) + + $Connection.Close() + #$table + + Remove-Item $LocalFile -Force -ErrorAction SilentlyContinue +} + +Write-Host 'All databases processed.' + +Stop-Transcript \ No newline at end of file diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Set-FW-ForAllInstances.ps1 b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Set-FW-ForAllInstances.ps1 new file mode 100644 index 000000000..a64db15c1 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/Set-FW-ForAllInstances.ps1 @@ -0,0 +1,53 @@ +$ErrorActionPreference = 'Stop' + +$logPath = 'C:\Windows\Temp\Set-FW-ForAllInstances.log' +Start-Transcript -Path $logPath -Append + +$Instances = @() +$BrowserService = Get-WmiObject -Class Win32_service | Where-Object { $_.Name -eq "SQLBrowser" } +if ($BrowserService) { + $Instance = New-Object PSObject + $Instance | Add-Member -MemberType NoteProperty -Name Instancename -Value "*SQLBROWSER*" + $SQLBrowser = $BrowserService.PathName + $Instance | Add-Member -MemberType NoteProperty -Name SQLBinnRoot -Value $SQLBrowser + + $Instances += $Instance +} + +$SQLInstances = @() +if ((Get-Item 'HKLM:\SOFTWARE\Microsoft').GetSubKeyNames() -contains 'Microsoft SQL Server') { + $SQLInstances = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server').InstalledInstances +} +foreach ($sql in $SQLInstances) { + $Instance = New-Object PSObject + $Instance | Add-Member -MemberType NoteProperty -Name Instancename -Value $sql + + $InstanceId = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL').("$sql") + $SQLBinnRoot = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$($InstanceId)\Setup").SQLBinRoot + '\sqlservr.exe' + $Instance | Add-Member -MemberType NoteProperty -Name SQLBinnRoot -Value $SQLBinnRoot + $Instances += $Instance +} + +foreach ($Instance in $Instances) { + + if ($Instance.Instancename -eq "*SQLBROWSER*") { + $FirewallRuleName = "SQL Server Browser" + } + else { + $FirewallRuleName = "SQL Server (" + $Instance.Instancename + ")" + } + $FirewallBinary = $Instance.SQLBinnRoot.Replace("""","") + $FWRule = Get-NetFirewallRule | where-object { $_.DisplayName -eq $FirewallRuleName } -ErrorAction SilentlyContinue + if ($FWRule) { + Write-Host $FirewallRuleName -ForegroundColor Yellow + Write-Host $FirewallBinary -ForegroundColor Yellow + Set-NetFirewallRule -NewDisplayName $FirewallRuleName -Program $FirewallBinary -Action Allow -Profile Domain,Private,Public -DisplayName $FirewallRuleName -Description $FirewallRuleName -Direction Inbound | Out-Null + } + else { + Write-Host $FirewallRuleName -ForegroundColor Green + Write-Host $FirewallBinary -ForegroundColor Green + New-NetFirewallRule -Program $FirewallBinary -Action Allow -Profile Domain,Private,Public -DisplayName $FirewallRuleName -Description $FirewallRuleName -Direction Inbound | Out-Null + } +} + +Stop-Transcript \ No newline at end of file diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/bootstrap-legacy.ps1 b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/bootstrap-legacy.ps1 new file mode 100644 index 000000000..90ecfd65a --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/bootstrap-legacy.ps1 @@ -0,0 +1,45 @@ +param( + [string]$BackupBaseUri, + [string]$StorageAccountName, + [string]$ManagedInstanceServer, + [string]$adminUsername, + [string]$adminPassword, + [string]$sqlMiAdminUsername, + [string]$sqlMiAdminPassword +) + +$ErrorActionPreference = 'Stop' + +$logPath = 'C:\Windows\Temp\bootstrap-legacy.log' +Start-Transcript -Path $logPath -Append + +Write-Host "Configuring SQL Firewall..." +& .\Set-FW-ForAllInstances.ps1 + +#Write-Host "Restoring Team Databases..." +# +#& .\Restore-TeamDatabases.ps1 -LabCount $LabCount -BackupBaseUri $BackupBaseUri -sqlusername $adminUsername -sqlpassword $adminPassword + +Write-Host "Downloading Team Databases..." + +& .\Download-TeamDatabases.ps1 -BackupBaseUri $BackupBaseUri + +Write-Host "Configuring legacy SQL Server..." + +& .\Configure-legacySQL.ps1 -sqlusername $adminUsername -sqlpassword $adminPassword + +Write-Host "Installing AzureCLI..." + +& .\Install-AzureCLI.ps1 + +Write-Host "Restoring Team Databases on SQLMI..." + +& .\Restore-TeamDatabasesMI.ps1 -BackupBaseUri $BackupBaseUri -sqlusername $sqlMiAdminUsername -sqlpassword $sqlMiAdminPassword -StorageAccountName $StorageAccountName -ManagedInstanceServer $ManagedInstanceServer + +Write-Host "Configuring SQLMI..." + +& .\Configure-SQLMI.ps1 -ManagedInstanceServer $ManagedInstanceServer -sqlusername $sqlMiAdminUsername -sqlpassword $sqlMiAdminPassword + +Write-Host "Bootstrap completed." + +Stop-Transcript \ No newline at end of file diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/bootstrap-newsql.ps1 b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/bootstrap-newsql.ps1 new file mode 100644 index 000000000..8f6d53199 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/bootstrap-newsql.ps1 @@ -0,0 +1,21 @@ +param( + [string]$BackupUri, + [string]$adminUsername, + [string]$adminPassword +) + +$ErrorActionPreference = 'Stop' + +$logPath = 'C:\Windows\Temp\bootstrap-newsql.log' +Start-Transcript -Path $logPath -Append + +Write-Host "Configuring SQL Firewall..." +& .\Set-FW-ForAllInstances.ps1 + +Write-Host "Restoring Sample Database..." + +& .\Restore-SampleDatabases.ps1 -BackupUri $BackupUri -sqlusername $adminUsername -sqlpassword $adminPassword + +Write-Host "Bootstrap completed." + +Stop-Transcript \ No newline at end of file diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/bootstrap-teamvm.ps1 b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/bootstrap-teamvm.ps1 new file mode 100644 index 000000000..8c1db7b83 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/bootstrap-teamvm.ps1 @@ -0,0 +1,45 @@ +param( + [string]$SamplesBaseUri, + [string]$LabsBaseUri, + [string]$BackupBaseUri, + [string]$adminUsername, + [string]$adminPassword, + [string]$WallpaperUri, + [string]$TeamName, + [string]$ManagedInstanceServer, + [string]$StorageAccountName, + [string]$ServerInstance +) + +$ErrorActionPreference = 'Stop' + +$logPath = 'C:\Windows\Temp\bootstrap-teamvm.log' +Start-Transcript -Path $logPath -Append + +Write-Host "Configuring TEAM VM..." + +##Not working with Bastion, so we will not set wallpaper for now. If you want to set wallpaper, you can run the following command after you RDP into the VM. +Write-Host "Configure Team Wallpaper..." +& .\Configure-TeamWallpaper.ps1 -WallpaperUri $WallpaperUri -TeamName $TeamName + +Write-Host "Restoring Team Databases..." +& .\Restore-TeamDatabases.ps1 -TeamName $TeamName -BackupBaseUri $BackupBaseUri -sqlusername $adminUsername -sqlpassword $adminPassword -ServerInstance $ServerInstance + +Write-Host "Configuring Team Databases..." +& .\Configure-legacySQL-DB.ps1 -TeamName $TeamName -sqlusername $adminUsername -sqlpassword $adminPassword -ServerInstance $ServerInstance + +Write-Host "Installing Team Tools..." +& .\install-team-tools.ps1 + +Write-Host "Downloading Sample Files..." +& .\Download-Samples.ps1 -SamplesBaseUri $SamplesBaseUri -ForceDownload + +Write-Host "Downloading LAB Files..." +& .\Download-Labs.ps1 -LabsBaseUri $LabsBaseUri -ForceDownload + +Write-Host "Configuring Teams Shortcuts..." +& .\Configure-Teams-Shortcuts.ps1 -ManagedInstanceServer $ManagedInstanceServer -StorageAccountName $StorageAccountName + +Write-Host "Bootstrap completed." + +Stop-Transcript \ No newline at end of file diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/configure.ps1 b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/configure.ps1 new file mode 100644 index 000000000..18c7be31d --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/configure.ps1 @@ -0,0 +1,80 @@ +$ErrorActionPreference = 'Stop' + +function Ensure-AzdValue { + param( + [Parameter(Mandatory)] + [string] $Name, + + [Parameter(Mandatory)] + [scriptblock] $Prompt, + + [switch] $Secret + ) + + $currentValue = azd env get-value $Name 2>$null + + if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrWhiteSpace($currentValue)) { + return + } + + $value = & $Prompt + + if ($Secret) { + #azd env set-secret $Name $value | Out-Null + azd env set $Name $value | Out-Null + } + else { + azd env set $Name $value | Out-Null + } +} + +Ensure-AzdValue ` + -Name 'VM_ADMIN_USERNAME' ` + -Prompt { + Read-Host 'VM administrator username' + } + +Ensure-AzdValue ` + -Name 'VM_ADMIN_PASSWORD' ` + -Secret ` + -Prompt { + # Beispiel im configure.ps1 Skript + $password = Read-Host -AsSecureString "VM administrator password" + # Konvertierung des SecureString in Klartext für azd (falls erforderlich) + $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password) + $plainPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($BSTR) + $plainPassword + } + +Ensure-AzdValue ` + -Name 'SQL_MI_ADMIN_USERNAME' ` + -Prompt { + Read-Host 'SQL MI administrator username' + } + +Ensure-AzdValue ` + -Name 'SQL_MI_ADMIN_PASSWORD' ` + -Secret ` + -Prompt { + # Beispiel im configure.ps1 Skript + $password = Read-Host -AsSecureString "SQL MI administrator password" + # Konvertierung des SecureString in Klartext für azd (falls erforderlich) + $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($password) + $plainPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($BSTR) + $plainPassword + } + +Ensure-AzdValue ` + -Name 'TEAM_VM_COUNT' ` + -Prompt { + do { + $value = Read-Host 'Number of team VMs, from 3 through 20' + $numericValue = $value -as [int] + } + until ( + $numericValue -ge 3 -and + $numericValue -le 20 + ) + + $numericValue + } \ No newline at end of file diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/install-team-tools.ps1 b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/install-team-tools.ps1 new file mode 100644 index 000000000..9001442fc --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/scripts/install-team-tools.ps1 @@ -0,0 +1,97 @@ +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +$logPath = 'C:\Windows\Temp\install-team-tools.log' + +Start-Transcript -Path $logPath -Append + +try { + [Net.ServicePointManager]::SecurityProtocol = + [Net.SecurityProtocolType]::Tls12 + + $installDirectory = 'C:\Install' + + New-Item ` + -Path $installDirectory ` + -ItemType Directory ` + -Force | + Out-Null + + Write-Host 'Downloading SQL Server Management Studio 22.' + + $ssmsInstaller = + Join-Path $installDirectory 'vs_SSMS.exe' + + Invoke-WebRequest ` + -Uri 'https://aka.ms/ssms/22/release/vs_SSMS.exe' ` + -OutFile $ssmsInstaller ` + -UseBasicParsing + + Write-Host 'Installing SQL Server Management Studio 22.' + + $ssmsProcess = Start-Process ` + -FilePath $ssmsInstaller ` + -ArgumentList '--add Microsoft.SqlServer.Workload.SSMS.AI --add Microsoft.SqlServer.Workload.SSMS.HybridAndMigration --includeRecommended --quiet --wait --norestart' ` + -Wait ` + -PassThru + + if ($ssmsProcess.ExitCode -notin 0, 3010) { + throw "SSMS installation failed with exit code $($ssmsProcess.ExitCode)." + } + + Write-Host 'Resolving latest Azure Storage Explorer release.' + + $release = Invoke-RestMethod ` + -Uri 'https://api.github.com/repos/microsoft/AzureStorageExplorer/releases/latest' ` + -UseBasicParsing + + $asset = $release.assets | + Where-Object { + $_.name -eq 'StorageExplorer-windows-x64.exe' + } | + Select-Object -First 1 + + if (-not $asset) { + throw 'The Azure Storage Explorer x64 installer could not be found.' + } + + $storageExplorerInstaller = + Join-Path $installDirectory 'StorageExplorer-windows-x64.exe' + + Write-Host 'Downloading Azure Storage Explorer.' + + Invoke-WebRequest ` + -Uri $asset.browser_download_url ` + -OutFile $storageExplorerInstaller ` + -UseBasicParsing + + Write-Host 'Installing Azure Storage Explorer.' + + $storageExplorerProcess = Start-Process ` + -FilePath $storageExplorerInstaller ` + -ArgumentList '/VERYSILENT /SUPPRESSMSGBOXES /NORESTART /ALLUSERS' ` + -Wait ` + -PassThru + + if ($storageExplorerProcess.ExitCode -notin 0, 3010) { + throw "Storage Explorer installation failed with exit code $($storageExplorerProcess.ExitCode)." + } + + New-Item ` + -Path 'HKLM:\SOFTWARE\SQLMicroHack' ` + -Force | + Out-Null + + New-ItemProperty ` + -Path 'HKLM:\SOFTWARE\SQLMicroHack' ` + -Name 'TeamToolsInstalled' ` + -Value (Get-Date).ToString('o') ` + -PropertyType String ` + -Force | + Out-Null + + Write-Host 'Team VM tool installation completed.' +} +finally { + Stop-Transcript +} \ No newline at end of file diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/shared-deploy-lab.ps1 b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/shared-deploy-lab.ps1 new file mode 100644 index 000000000..17bb4e933 --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/shared-deploy-lab.ps1 @@ -0,0 +1,205 @@ +<# +.SYNOPSIS +Hook that runs once per subscription before any deploy-lab.ps1 run starts. +.DESCRIPTION +Hook that runs before the per-user deploy-lab.ps1 runs are started. It can be used to deploy shared resources on a subscription level +(e.g. a hub VNet) or to prepare the subscription once instead of once per lab (e.g. registering resource providers). +If it fails for any subscription, no deploy-lab.ps1 runs at all. Emitted HackboxCredential entries are stored for every lab in this subscription. +.PARAMETER SubscriptionId +Specifies the Azure subscription that contains the lab resources. +.PARAMETER PreferredLocation +Specifies the preferred Azure regions (ordered by preference) for resource deployment. +.PARAMETER AllowedEntraUserIds +Entra user object IDs of every participant holding a lab in this subscription. +#> +param( + [Parameter(Mandatory=$true)] + [string]$SubscriptionId, + + [Parameter(Mandatory=$true)] + [string[]]$PreferredLocation = @(), + + [Parameter(Mandatory=$false)] + [string[]]$AllowedEntraUserIds = @() +) + +$ErrorActionPreference = 'Stop' + +$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +Write-Host "[$SubscriptionId] Running shared-deploy-lab.ps1 in $scriptPath" + +# EXAMPLE: Register Resource Providers here if needed +$requiredProviders = @( + "Microsoft.Compute", + "Microsoft.Network", + "Microsoft.Storage", + "Microsoft.Sql", + "Microsoft.SqlVirtualMachine", + "Microsoft.DevTestLab" +) +foreach($provider in $requiredProviders) { + $state = (Get-AzResourceProvider -ProviderNamespace $provider -ErrorAction SilentlyContinue | Select-Object -First 1).RegistrationState + if($state -ne "Registered") { + Write-Host "[$SubscriptionId] Registering resource provider: $provider" + Register-AzResourceProvider -ProviderNamespace $provider -ErrorAction Stop + } + else { + Write-Host "[$SubscriptionId] Resource provider $provider is already registered." + } +} + +try { + $registrationState = Get-AzProviderFeature -ProviderNamespace Microsoft.Network -FeatureName AllowBringYourOwnPublicIpAddress + If ($registrationState.RegistrationState -ne "Registered") { + Register-AzProviderFeature -ProviderNamespace Microsoft.Network -FeatureName AllowBringYourOwnPublicIpAddress + } +} +catch { + Write-Host "Could not register AllowBringYourOwnPublicIpAddress." -ForegroundColor Red +} + +# EXAMPLE: shared resources, e.g. hub networks, bastion host, etc. can be deployed here... +# no resource group is pre-created for this hook, so pick your own name and never use a participant's resource group +$environmentName = "sqlhack" # template produces rg-sqlhack-shared +$sharedResourceGroup = "rg-${environmentName}-shared" +$adminUsername = "DemoUser" +$adminPassword = New-MhhStablePassword -Purpose 'vm-admin' -SubscriptionId $SubscriptionId -ResourceGroupName "Default" +$sqlMiAdminUsername = "DemoUser" +$sqlMiAdminPassword = New-MhhStablePassword -Purpose 'sqlmi-admin' -SubscriptionId $SubscriptionId -ResourceGroupName "Default" +$legacySQLName = "legacySQL2016" +$arcSQLName = "arcSQL2022" + + +# deploy shared resources in the shared resource group (do not forget to assign the correct rbac roles to the allowed Entra users) +# Invoke-MhhDeploymentWithRegionFallback creates/recreates the resource group, re-grants Owner and falls back to the next region +$templatePath = Join-Path $scriptPath "infra" +$template = Join-Path $templatePath "main-shared.bicep" +Write-Host "[$SubscriptionId] Deploying shared resources from template $template to resource group $sharedResourceGroup" + +Write-Host $AllowedEntraUserIds + +New-AzResourceGroup -Name $sharedResourceGroup -Location $PreferredLocation[0] -Force -ErrorAction Stop + +$tags = @{ + SecurityControl = "Ignore" +} + +$result = Invoke-MhhDeploymentWithRegionFallback ` + -PreferredLocations $PreferredLocation ` + -RgOwnerEntraObjectIds @($AllowedEntraUserIds) ` + -ResourceGroupName $sharedResourceGroup ` + -TemplateFile $template ` + -Tag $tags ` + -TemplateParameterObject @{ + environmentName = $environmentName # z.B. "sqlhack" + location = $PreferredLocation[0] + adminUsername = $adminUsername + adminPassword = $adminPassword + sqlMiAdminUsername = $sqlMiAdminUsername + sqlMiAdminPassword = $sqlMiAdminPassword + legacySQLName = $legacySQLName + arcSQLName = $arcSQLName + } + +Write-Host "[$SubscriptionId] Result: $($result)" + +# You can send back information to the hackbox console (credentials) - Simply return a hashtable like this: +# Note: everything returned here is shown to EVERY participant in this subscription, so never emit per-participant secrets. +# @{"HackboxCredential" = @{ name = "AdminPassword" ; value = "TopSecret"; note = "Useful info here" }} + +$managedInstance = Get-AzSqlInstance -ResourceGroupName $sharedResourceGroup -ErrorAction SilentlyContinue | Select-Object -First 1 +$managedInstanceName = $managedInstance.ManagedInstanceName +$managedInstanceFQDN = $managedInstance.FullyQualifiedDomainName +[string]$managedInstanceResourceId = $managedInstance.Id +#$managedInstancePrincipalId = $managedInstance.Identity.PrincipalId + +try { + Write-Host "Connecting to MGraph..." + $token = (Get-AzAccessToken -ResourceTypeName MSGraph).Token + Connect-MgGraph -AccessToken $token -NoWelcome -Erroraction Stop + Write-Host "Calling Set-MhhManagedIdentityRoleMember for $managedInstanceResourceId ..." + $return = Set-MhhManagedIdentityRoleMember -ResourceId $managedInstanceResourceId + Write-Host "Calling Set-MhhManagedIdentityRoleMember returned $return ..." +} +catch { + Write-Host "Failed to grant 'Directory Readers' to Managed Identity of $managedInstanceFQDN." -ForegroundColor Red + $ErrorString = $_ | format-list -force | Out-String + Write-Host "ERR: $ErrorString" -ForegroundColor Red + Write-Host $return +} + +Start-Sleep -Seconds 30 + +try { + $FirstLabUser = Get-MhhLabUser -UserId @($AllowedEntraUserIds) | Where-Object { $_.ShortName.ToLower() -match "labuser-[0-9]{4}"} | Sort-Object -Property ShortName | Select-Object -First 1 + $SQLMiEntraAdmin = Get-AzADUser -ObjectId $FirstLabUser.Id -ErrorAction Stop + # Entra Admin auf der MI setzen + #$return = Set-AzSqlInstanceActiveDirectoryAdministrator ` + # -ResourceGroupName $sharedResourceGroup ` + # -InstanceName $managedInstance.ManagedInstanceName ` + # -DisplayName $SQLMiEntraAdmin.DisplayName ` + # -ObjectId $SQLMiEntraAdmin.Id -ErrorAction Stop + + Write-Host "Configuring $($SQLMiEntraAdmin.UserPrincipalName) as Entra ID Admin for SQLMI ..." + # Concatenate the query string; "$var?api-version=..." interpolates as a scoped variable. + $adminPath = $managedInstance.Id + '/administrators/ActiveDirectory?api-version=2023-08-01-preview' + $payload = @{ + properties = @{ + administratorType = 'ActiveDirectory' + login = $SQLMiEntraAdmin.UserPrincipalName + sid = $SQLMiEntraAdmin.Id + tenantId = (Get-AzContext).Tenant.Id + } + } | ConvertTo-Json -Depth 5 + $resp = Invoke-AzRestMethod -Method PUT -Path $adminPath -Payload $payload + Write-Host "Configuring $($SQLMiEntraAdmin.UserPrincipalName) as Entra ID Admin for SQLMI returned $($resp.StatusCode) ..." + if ($resp.StatusCode -notin 200, 201, 202) { + throw "Set MI Entra admin failed (HTTP $($resp.StatusCode)): $($resp.Content)" + } +} +catch { + Write-Host "Failed to set Entra ID Admin on $managedInstanceFQDN." -ForegroundColor Red + $ErrorString = $_ | format-list -force | Out-String + Write-Host "ERR: $ErrorString" -ForegroundColor Red +} + +$timeoutSeconds = 120 +$pollIntervalSeconds = 10 +$found = $false +for ($elapsed = 0; $elapsed -lt $timeoutSeconds; $elapsed += $pollIntervalSeconds) +{ + Write-Host "Checking Entra ID Admin ($elapsed seconds)..." + try + { + $admin = Get-AzSqlInstanceActiveDirectoryAdministrator ` + -ResourceGroupName $sharedResourceGroup ` + -InstanceName $managedInstanceName ` + -ErrorAction Stop + Write-Host "Expected Admin (ID): $($SQLMiEntraAdmin.Id) - Current Admin (ID): $($admin.ObjectId)" + if ($admin.ObjectId -eq $SQLMiEntraAdmin.Id) + { + Write-Host "Expected Entra ID Admin is configured." + + $found = $true + break + } + } + catch + { + Write-Host "No Entra ID Admin configured yet." + } + Start-Sleep -Seconds $pollIntervalSeconds +} +if (-not $found) +{ + throw "Expected Entra ID Admin was not configured within 2 minutes." +} + +@{"HackboxCredential" = @{name = 'Legacy SQL Server Name'; value = $legacySQLName; note = 'Name of legacy SQL Server'}} +@{"HackboxCredential" = @{name = 'SQLMI FQDN'; value = $managedInstanceFQDN; note = 'FQDN of SQLMI'}} +@{"HackboxCredential" = @{name = 'Arc SQL Server Name'; value = $arcSQLName; note = 'Name of new SQL Server'}} + +@{"HackboxCredential" = @{name = 'VM User Name'; value = $adminUsername; note = 'Username to connect for every VM'}} +@{"HackboxCredential" = @{name = 'VM User Password'; value = $adminPassword; note = 'Password to connect to every VM'}} +@{"HackboxCredential" = @{name = 'SQLMI User Name'; value = $sqlMiAdminUsername; note = 'Username to connect to SQLMI'}} +@{"HackboxCredential" = @{name = 'SQLMI User Password'; value = $sqlMiAdminPassword; note = 'Password to connect to SQLMI'}} diff --git a/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/tools/CreateEntraIDUsersAndPermissions.ps1 b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/tools/CreateEntraIDUsersAndPermissions.ps1 new file mode 100644 index 000000000..376c0c15b --- /dev/null +++ b/03-Azure/01-02 Data/02-SQL_Modernization/labautomation/tools/CreateEntraIDUsersAndPermissions.ps1 @@ -0,0 +1,53 @@ +#Connect-AzAccount +#Install-Module -Name AzureAD +#Set-ExecutionPolicy -ExecutionPolicy Unrestricted + + +$user_group="SQLHackUsers" +$HackRG ="rg-sqlhack" + +$group = get-AzAdgroup -DisplayName $user_group + +if(!$group) +{ + +$group= New-AzADGroup -DisplayName $user_group -MailNickname $user_group +} +$username="sqlhackuser" +$domain="1dayopenhack.biz" + +$count=1..25 + +$groupid=$group.Id + +foreach ($i in $count) +{ + $i_string = $i | % tostring 00 + $password="&sqlhack@demo_"+$i_string+"!" + $securepassword= ConvertTo-SecureString $password -AsPlainText -force + $upn=$username+$i_string +"@"+$domain + $user = Get-AzADUser -UserPrincipalName $upn + if(!$user) + { + Write-Host "Creating user $upn" + $member=New-AzADUser -DisplayName $username$i_string -Password $securepassword -AccountEnabled $true -MailNickname $username$i_string -UserPrincipalName $upn + Add-AzADGroupMember -TargetGroupObjectId $groupid -MemberObjectId $member.Id + } + else + { + Write-Host "User $upn already exists" + } + +} + +# Create AdminUser (RUN THIS PART IF YOU ONLY NEED AN ADDITIONAL ADMIN USER FOR MULTIPLE COACHES) + # $username ="adminuser" (RUN THIS PART IF YOU ONLY NEED AN ADDITIONAL SQL MI ADMIN USER FOR MULTIPLE COACHES) + # $password = + # $securepassword= ConvertTo-SecureString $password -AsPlainText -force + # $upn=$username+"@"+$domain + # $member=New-AzADUser -DisplayName $username$i_string -Password $securepassword -AccountEnabled $true -MailNickname $username$i_string -UserPrincipalName $upn + +#Get-AzRoleDefinition | Format-Table -Property Name, IsCustom, Id + +New-AzRoleAssignment -ObjectId $groupid -RoleDefinitionName Reader -Scope (Get-AzResourceGroup -Name $HackRG).ResourceId +