diff --git a/samples/manage/azure-arc-enabled-sql-server/install-payg-sql-server/README.md b/samples/manage/azure-arc-enabled-sql-server/install-payg-sql-server/README.md new file mode 100644 index 00000000..fd67a5b8 --- /dev/null +++ b/samples/manage/azure-arc-enabled-sql-server/install-payg-sql-server/README.md @@ -0,0 +1,69 @@ +# Overview + +This script installs a pay-as-you-go SQL Server instance on your machine and automatically connects it to Azure using a downloaded SQL Server media. + +# Prerequisites + +- You have met the [onboarding prerequisites](https://learn.microsoft.com/sql/sql-server/azure-arc/prerequisites). +- You have downloaded a SQL Server image file from the workspace provided by Microsoft technical support. To obtain it, open a support request using the "Get SQL Installation Media" subcategory and specify the desired version and edition. +- You are logged in to the machine with an administrator account. +- If you are installing SQL Server on Windows Server 2016, you have a secure TLS configuration as described below. + + +# Mitigating the TLS version issue on Windows Server 2016 + +When running the script on Windows Server 2016, the OS may be configured with a TLS version that does not meet the Azure security requirements. You need to enable strong TLS versions (TLS 1.2 and 1.3) when they are available, while still supporting older TLS versions (1.0 and 1.1) when TLS 1.2 and 1.3 are unavailable. You need to also disable versions SSL2 and SSL3, which are insecure. + +To see if you need to make the change, run the command below from an elevated PowerShell prompt. +```PowerShell +[Net.ServicePointManager]::SecurityProtocol +``` + +If the result is `SSL3, Tls`, you need to fix the TLS version by running the following commands. + +```PowerShell +Set-ItemProperty -Path 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\.NetFramework\v4.0.30319' -Name 'SchUseStrongCrypto' -Value '1' -Type DWord +Set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\.NetFramework\v4.0.30319' -Name 'SchUseStrongCrypto' -Value '1' -Type DWord +``` + +After running these commands, reboot the machine (in case currently-running applications were referencing previous values). To verify that the changes were applied correctly, run this command again: + +```PowerShell +[Net.ServicePointManager]::SecurityProtocol +``` +The result should be `Tls, Tls11, Tls12, Tls13` + +# Downloading the script + +To download the script to your current folder run: + +```console +curl https://raw.githubusercontent.com/microsoft/sql-server-samples/master/samples/manage/azure-arc-enabled-sql-server/install-payg-sql-server/install-payg-sql-server.ps1 -o install-payg-sql-server.ps1 +``` + +# Launching the script + +The script must be run in an elevated PowerShell session. It accepts the following command line parameters: + +| **Parameter**                                         | **Value**                                                                       | **Description** | +|:--|:--|:--| +|-AzureSubscriptionId|subscription_id|Required: Subscription id that will contain the Arc-enabled machine and Arc-enable SQL Server resources. That subscription will be billed for SQL Server software using a pay-as-you-go method. | +|-AzureResourceGroup |resource_group_name|Required: Resource group that will contain the Arc-enabled machine and Arc-enable SQL Server resource.| +|-AzureRegion |region name| Required: the region to store the machuine and SQL Server meta-data. | +|-SqlServerInstanceName | name of the instance|Optional: the machine name will be used if not specified| +|-SqlServerAdminAccounts | SQL Server admin accounts | Optional. By default "BUILTIN\ADMINISTRATORS" will be used.| +|-SqlServerSvcAccount| SQL Server services account |Optional. By default "NT AUTHORITY\NETWORK SERVICE" will be used.| +|-SqlServerSvcPassword| SQL Server service account password| Required if a custom service account is specified.| +|-AgtServerSvcAccount|SQL Agent service account|Optional. By default "NT AUTHORITY\NETWORK SERVICE" will be used.| +|-AgtServerSvcPassword|SQL Agent service account password|Required if a custom service account is specified.| +|-IsoFolder|Folder path|Required. The folder containing the files downloaded from the workspace.| +|-Proxy|HTTP proxy URL|Optional. Needed if your networks is configured with an HTTP proxy.| + +# Example + +The following command installs a SQL Server instance from the Downloads folder, connects it to subscription ID ``, resource group `` in the West US region, and configures it with LicenseType=PAYG. It uses the default admin and service accounts, and uses a direct connection to Azure. + +```PowerShell +.\install-payg-sql-server.ps1 -AzureSubscriptionId -AzureResourceGroup -AzureRegion westus -IsoFolder C:\Users\[YourUsername]\Downloads + +``` diff --git a/samples/manage/azure-arc-enabled-sql-server/install-payg-sql-server/install-payg-sql-server.ps1 b/samples/manage/azure-arc-enabled-sql-server/install-payg-sql-server/install-payg-sql-server.ps1 index 6ff5d8f1..0a4efb63 100644 --- a/samples/manage/azure-arc-enabled-sql-server/install-payg-sql-server/install-payg-sql-server.ps1 +++ b/samples/manage/azure-arc-enabled-sql-server/install-payg-sql-server/install-payg-sql-server.ps1 @@ -2,25 +2,25 @@ param ( [Parameter (Mandatory=$true)] [string]$AzureSubscriptionId, [Parameter (Mandatory=$true)] - [string]$AzureResourceGroupUri, + [string]$AzureResourceGroup, [Parameter (Mandatory=$true)] [string]$AzureRegion, [Parameter (Mandatory=$false)] [string]$SqlServerInstanceName, - [Parameter (Mandatory=$true)] - [string]$SqlServerAdminAccounts, - [Parameter (Mandatory=$true)] - [string]$SqlServerSvcAccount, - [Parameter (Mandatory=$true)] + [Parameter (Mandatory=$false)] + [string]$SqlServerAdminAccounts = "BUILTIN\ADMINISTRATORS", + [Parameter (Mandatory=$false)] + [string]$SqlServerSvcAccount = "NT AUTHORITY\NETWORK SERVICE", + [Parameter (Mandatory=$false)] [string]$SqlServerSvcPassword, + [Parameter (Mandatory=$false)] + [string]$AgtServerSvcAccount = "NT AUTHORITY\NETWORK SERVICE", + [Parameter (Mandatory=$false)] + [string]$AgtServerSvcPassword, [Parameter (Mandatory=$true)] - [string]$SqlServerVersion, - [Parameter (Mandatory=$true)] - [string]$SqlServerEdition, - [Parameter (Mandatory=$true)] - [string]$SqlServerProductKey, - [Parameter (Mandatory=$true)] - [string]$isoURL + [string]$IsoFolder, + [Parameter (Mandatory=$false)] + [string]$Proxy ) # This function checks if the specified module is imported into the session and if not installes and/or imports it @@ -53,7 +53,7 @@ function LoadModule # If module is not imported, not available on disk, but is in online gallery then install and import if (Find-Module -Name $name) { - Install-Module -Name $name -Force -Verbose -Scope CurrentUser + Install-Module -Name $name -Force -Scope CurrentUser try { Import-Module $name -ErrorAction SilentlyContinue @@ -78,8 +78,8 @@ function LoadModule } try { - - #Step 0: Ensure PS version and load missing Azure modules + + write-host "==== Ensure PS version and load missing Azure modules ====" # # Suppress warnings # @@ -89,121 +89,221 @@ try { $requiredModules = @( "AzureAD", "Az.Accounts", + "Az.Resources", "Az.ConnectedMachine", "Az.ResourceGraph" ) $requiredModules | Foreach-Object {LoadModule $_} + + write-host "==== Check if setup.exe is already running and kill it if so ====" - # Step 1: Check if setup.exe is already running and kill it if so if (Get-Process setup -ErrorAction SilentlyContinue) { Stop-Process -Name setup -Force Write-Host "Existing setup.exe process terminated." } - # Step 2: Log in to Azure - Connect-AzAccount + write-host "==== Log in to Azure ====" + + Update-AzConfig -EnableLoginByWam $false + Connect-AzAccount | Out-Null $subscription = Get-AzSubscription -SubscriptionId $AzureSubscriptionId -ErrorAction SilentlyContinue if (-not $subscription) { Write-Error "Azure subscription with ID '$AzureSubscriptionId' does not exist." exit } + Set-AzContext -Subscription $AzureSubscriptionId | Out-Null - # Step 2: Block auto-onboarding to Arc by tagging the resource group - $existingResourceGroup = Get-AzResourceGroup -Name $AzureResourceGroupUri -ErrorAction SilentlyContinue + write-host "==== Block auto-onboarding to Arc ====" + + $existingResourceGroup = Get-AzResourceGroup -Name $AzureResourceGroup -ErrorAction SilentlyContinue if ($existingResourceGroup) { - Write-Host "Resource group '$AzureResourceGroupUri' exists." + $tags = @{"ArcOnboarding" = "Blocked"} + Set-AzResourceGroup -Name $AzureResourceGroup -Tag $tags | Out-Null } else { - Write-Error "Resource group '$AzureResourceGroupUri' does not exist." + Write-Error "Resource group '$AzureResourceGroup' does not exist." exit } - $tags = @{"ArcOnboarding" = "Blocked"} - Set-AzResourceGroup -Name $AzureResourceGroupUri -Tag $tags - # Step 3: Onboard the VM to Azure Arc - $hostName = (Get-WmiObject Win32_ComputerSystem).Name + write-host "==== Mount the ISO file as a volume ====" - New-AzConnectedMachine -ResourceGroupName $AzureResourceGroupUri -Name $hostName -Location $AzureRegion - - # Step 4: Automatically download installable media + # Retrieve the product key if any - $isoLocation = "C:\download\SQLServer.iso" - if (!(Test-Path -Path $isoLocation)) { - $freeSpace = (Get-PSDrive -Name C).Free - $isoSize = (Invoke-WebRequest -Uri $isoURL -Method Head).Headers.'Content-Length' - if ($freeSpace -gt $isoSize) { - Start-BitsTransfer -Source $isoURL -Destination $isoLocation + $keyFiles = (Get-ChildItem $IsoFolder -Filter "*.txt") + $productKey = "" + foreach ($keyFile in $keyFiles) { + # Read each line from the file + Get-Content $keyFile.fullname | ForEach-Object { + if ($_ -match "[A-Z0-9]{5,}-[A-Z0-9]{5,}-[A-Z0-9]{5,}-[A-Z0-9]{5,}-[A-Z0-9]{4,}.*") { + # Extract the product key (including any following string after a space) + $productKey = [regex]::Match($_, '[A-Z0-9]{5,}-[A-Z0-9]{5,}-[A-Z0-9]{5,}-[A-Z0-9]{5,}-[A-Z0-9]{4,}.*').Value + # Strip any text after the product key + $productKey = $productKey -replace " .*$" + } + } + } + + + $isoFiles = (Get-ChildItem $IsoFolder -Filter "*.iso") + + + # Pick the .iso file and mount + + $noKeylist = "SQLFULL_ENU_ENTVL.iso", "SQLFULL_ENU_STDVL.iso", "SQLServer2022-x64-ENU-Ent.iso", "SQLServer2022-x64-ENU-Std.iso" + + foreach ($isoFile in $isoFiles) { + write-host("****isoFile: $($isoFile)") + $imagePath = $isoFile.FullName + if ($noKeylist -contains $isoFile.Name) {$productKey = ""} + if (!(Get-DiskImage -ImagePath $imagePath).Attached) { + $mountResult = Mount-DiskImage -ImagePath $imagePath -PassThru } else { - throw "Not enough free space to download the ISO." + $mountResult = Get-DiskImage -ImagePath $imagePath } + $driveLetter = ($mountResult | Get-Volume).DriveLetter + Write-Host "ISO file $($isoFile.Name) mounted as drive $($driveLetter):" + break } - # Step 5: Mount the ISO file as a volume - $volumeInfo = Mount-DiskImage -ImagePath $isoLocation -PassThru | Get-Volume - # Step 6: Run unattended SQL Server setup from the mounted volume - $setupPath = ($volumeInfo.DriveLetter + ":\setup.exe") - $argumentList = " - /q - /ACTION=Install - /FEATURES=SQL - /INSTANCEDIR=C:\SQL - /SQLSYSADMINACCOUNTS='$($SqlServerAdminAccounts)' - /SQLSVCACCOUNT='$($SqlServerSvcAccount)' - /SQLSVCPASSWORD='$($SqlServerSvcPassword)' - /AGTSVCACCOUNT='$($SqlServerSvcAccount)' - /AGTSVCPASSWORD='$($SqlServerSvcPassword)' - /IACCEPTSQLSERVERLICENSETERMS - /PID='$($SqlServerProductKey)' - /Edition='$($SqlServerEdition)' - " + write-host "==== Run unattended SQL Server setup from the mounted volume ====" + + + # Launch setup + + $setupPath = ($driveLetter + ":\setup.exe") + + + $argumentList = "/q /ACTION=Install /FEATURES=SQL /SQLSVCACCOUNT=`"$($SqlServerSvcAccount)`" /SQLSYSADMINACCOUNTS=`"$($SqlServerAdminAccounts)`" /AGTSVCACCOUNT=`"$($AgtServerSvcAccount)`" /IACCEPTSQLSERVERLICENSETERMS" + # some optional arguments if ($SqlServerInstanceName) { - $argumentList += "/INSTANCENAME='$($SqlServerInstanceName)'" + $argumentList += " /INSTANCENAME= $($SqlServerInstanceName)" + } + if ($productKey) { + $argumentList += " /PID=`"$($productKey)`"" } - Start-Process -FilePath $setupPath -ArgumentList $argumentList + if ($SqlServerSvcPassword) { + $argumentList += " /SQLSVCPASSWORD=`"$($SqlServerSvcPassword)`"" + } + + if ($AgtSvCPassword) { + $argumentList += " /AGTSVCPASSWORD=`"$($AgtSvCPassword)`"" + } + + + Start-Process -Wait -FilePath $setupPath -ArgumentList $argumentList -RedirectStandardOutput setup-output.txt + + + Dismount-DiskImage -ImagePath $imagePath | Out-Null + + write-host "==== Onboard the VM to Azure Arc ====" + + $hostName = (Get-WmiObject Win32_ComputerSystem).Name + + if ($Proxy) { + Connect-AzConnectedMachine -ResourceGroupName $AzureResourceGroup -Name $hostName -Location $AzureRegion -Proxi $Proxy | Out-Null + } else { + Connect-AzConnectedMachine -ResourceGroupName $AzureResourceGroup -Name $hostName -Location $AzureRegion | Out-Null + } + + write-host "==== Install SQL Arc extension with LT=PAYG and upgrade to the latest version ====" + - # Step 7: Install SQL Arc extension with LT=PAYG $Settings = @{ SqlManagement = @{ IsEnabled = $true }; LicenseType = "PAYG"; - enableExtendedSecurityUpdates = $True; - esuLastUpdatedTimestamp = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ss.fffZ') + enableExtendedSecurityUpdates = $False; } - New-AzConnectedMachineExtension -ResourceGroupName $AzureResourceGroupUri -MachineName $hostName -Name "WindowsAgent.SqlServer" -Publisher "Microsoft.AzureData" -Type "WindowsAgent.SqlServer" -TypeHandlerVersion "1.0" -Settings $settings - # Step 9: Dismount the ISO file after installation - Dismount-DiskImage -ImagePath $isoLocation + + New-AzConnectedMachineExtension -ResourceGroupName $AzureResourceGroup -MachineName $hostName -Name "WindowsAgent.SqlServer" -Publisher "Microsoft.AzureData" -ExtensionType "WindowsAgent.SqlServer" -Location $AzureRegion -Settings $Settings -EnableAutomaticUpgrade - # Step 10: Remove the media from the local file system - Remove-Item -Path $isoLocation + + write-host "==== Display the status of the billable Arc-enabled host ====" - # Step 8: Display the status of the Azure resource for Arc-enabled SQL Server $query = " resources - | where type =~ 'microsoft.hybridcompute/machines' - | where resourceGroup =~ '$($AzureResourceGroupUri)' - | where properties.detectedProperties.mssqldiscovered == 'true' - | extend machineIdHasSQLServerDiscovered = id - | project name, machineIdHasSQLServerDiscovered, resourceGroup, subscriptionId - | join kind= leftouter ( - resources - | where type == 'microsoft.hybridcompute/machines/extensions' | where properties.type in ('WindowsAgent.SqlServer','LinuxAgent.SqlServer') - | extend machineIdHasSQLServerExtensionInstalled = iff(id contains '/extensions/WindowsAgent.SqlServer' or id contains '/extensions/LinuxAgent.SqlServer', substring(id, 0, indexof(id, '/extensions/')), '') - | project Extension_State = properties.provisioningState, - License_Type = properties.settings.LicenseType, - ESU = iff(notnull(properties.settings.enableExtendedSecurityUpdates), iff(properties.settings.enableExtendedSecurityUpdates == true,'enabled','disabled'), ''), - Extension_Version = properties.instanceView.typeHandlerVersion, - machineIdHasSQLServerExtensionInstalled)on $left.machineIdHasSQLServerDiscovered == $right.machineIdHasSQLServerExtensionInstalled - | where isnotempty(machineIdHasSQLServerExtensionInstalled) - | project-away machineIdHasSQLServerDiscovered, machineIdHasSQLServerExtensionInstalled + | where type =~ 'Microsoft.HybridCompute/machines' + | where subscriptionId =~ '$($AzureSubscriptionId)' + | where resourceGroup =~ '$($AzureResourceGroup)' + | extend status = tostring(properties.status) + | where status =~ 'Connected' + | extend machineID = tolower(id) + | extend VMbyManufacturer = toboolean(iff(properties.detectedProperties.manufacturer in ( + 'VMware', + 'QEMU', + 'Amazon EC2', + 'OpenStack', + 'Hetzner', + 'Mission Critical Cloud', + 'DigitalOcean', + 'UpCloud', + 'oVirt', + 'Alibaba', + 'KubeVirt', + 'Parallels', + 'XEN' + ), 1, 0)) + | extend VMbyModel = toboolean(iff(properties.detectedProperties.model in ( + 'OpenStack', + 'Droplet', + 'oVirt', + 'Hypervisor', + 'Virtual', + 'BHYVE', + 'KVM' + ), 1, 0)) + | extend GoogleVM = toboolean(iff((properties.detectedProperties.manufacturer =~ 'Google') and (properties.detectedProperties.model =~ 'Google Compute Engine'), 1, 0)) + | extend NutanixVM = toboolean(iff((properties.detectedProperties.manufacturer =~ 'Nutanix') and (properties.detectedProperties.model =~ 'AHV'), 1, 0)) + | extend MicrosoftVM = toboolean(iff((properties.detectedProperties.manufacturer =~ 'Microsoft Corporation') and (properties.detectedProperties.model =~ 'Virtual Machine'), 1, 0)) + | extend billableCores = iff(VMbyManufacturer or VMbyModel or GoogleVM or NutanixVM or MicrosoftVM, properties.detectedProperties.logicalCoreCount, properties.detectedProperties.coreCount) + | join kind = leftouter // Join Extension + ( + resources + | where type =~ 'Microsoft.HybridCompute/machines/extensions' + | where name == 'WindowsAgent.SqlServer' or name == 'LinuxAgent.SqlServer' + | extend extMachineID = substring(id, 0, indexof(id, '/extensions')) + | extend extensionId = id + ) + on `$left.id == `$right.extMachineID + | join kind = inner // Join SQL Arc + ( + resources + | where type =~ 'microsoft.azurearcdata/sqlserverinstances' + | extend sqlVersion = tostring(properties.version) + | extend sqlEdition = tostring(properties.edition) + | extend is_Enterprise = toint(iff(sqlEdition == 'Enterprise', 1, 0)) + | extend sqlStatus = tostring(properties.status) + | extend licenseType = tostring(properties.licenseType) + | where sqlEdition in ('Enterprise', 'Standard') + | where licenseType !~ 'HADR' + | where sqlStatus =~ 'Connected' + | extend ArcServer = tolower(tostring(properties.containerResourceId)) + | order by sqlEdition + ) + on `$left.machineID == `$right.ArcServer + | where isnotnull(extensionId) + | summarize Edition = iff(sum(is_Enterprise) > 0, 'Enterprise', 'Standard') by machineID + , name + , resourceGroup + , subscriptionId + , Model = tostring(properties.detectedProperties.model) + , Manufacturer = tostring(properties.detectedProperties.manufacturer) + , License_Type = tostring(properties1.settings.LicenseType) + , OS = tostring(properties.osName) + , Uses_UV = tostring(properties1.settings.UsePhysicalCoreLicense.IsApplied) + , Cores = tostring(billableCores) + , Version = sqlVersion + | project-away machineID + | order by Edition, name asc " - Search-AzGraph -Query "$($query)" + Search-AzGraph -Query $query } catch { Write-Error "An error occurred: $_" # You can add additional error handling logic here } finally { # Cleanup or other actions that should always run - Write-Host "Script execution completed." + Write-Host "==== Installation completed ====" }