diff --git a/extensions/resource-deployment/images/azure-sql-vm.svg b/extensions/resource-deployment/images/azure-sql-vm.svg new file mode 100644 index 0000000000..2676d16464 --- /dev/null +++ b/extensions/resource-deployment/images/azure-sql-vm.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + Icon-databases-124 + + + + + + + + diff --git a/extensions/resource-deployment/notebooks/azurevm/create-sqlvm.ipynb b/extensions/resource-deployment/notebooks/azurevm/create-sqlvm.ipynb new file mode 100644 index 0000000000..90595e2f30 --- /dev/null +++ b/extensions/resource-deployment/notebooks/azurevm/create-sqlvm.ipynb @@ -0,0 +1,396 @@ +{ + "metadata": { + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python", + "version": "3.6.6", + "mimetype": "text/x-python", + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "pygments_lexer": "ipython3", + "nbconvert_exporter": "python", + "file_extension": ".py" + } + }, + "nbformat_minor": 2, + "nbformat": 4, + "cells": [ + { + "cell_type": "markdown", + "source": [ + "Create Azure SQL Virtual Machine\n", + "============================================\n", + "\n", + "Steps of this procedure include:\n", + "1. Set variables and set up Notebook \n", + "1. Connect to Azure account and subscription \n", + "1. Configure Network Settings \n", + "1. Provision virtual machine resource in Azure \n", + "1. Provision SQL VM resource in Azure" + ], + "metadata": { + "azdata_cell_guid": "e479b550-d6bd-49c5-965a-34a7d1d16412" + } + }, + { + "cell_type": "markdown", + "source": [ + "## Set variables\r\n", + "These variables are set based on your inputs in the deployment wizard. You can make changes to these variables but be aware of possible validation errors caused by your changes.\r\n", + "\r\n", + "\r\n", + "\r\n", + "\r\n", + "" + ], + "metadata": { + "azdata_cell_guid": "37db2e50-dcde-4dd5-820c-7dc11212f1eb" + } + }, + { + "cell_type": "markdown", + "source": [ + "## Notebook setup" + ], + "metadata": { + "azdata_cell_guid": "6251b830-476f-48f2-af1e-3680b541e455" + } + }, + { + "cell_type": "code", + "source": [ + "import sys, json, time, string, random, subprocess\n", + "\n", + "if \"AZDATA_NB_VAR_AZURE_SQLVM_PASSWORD\" in os.environ:\n", + " azure_sqlvm_password = os.environ[\"AZDATA_NB_VAR_AZURE_SQLVM_PASSWORD\"]\n", + "\n", + "if \"AZDATA_NB_VAR_AZURE_SQLVM_SQL_PASSWORD\" in os.environ:\n", + " azure_sqlvm_sqlAuthenticationPassword = os.environ[\"AZDATA_NB_VAR_AZURE_SQLVM_SQL_PASSWORD\"]\n", + "\r\n", + "def run_command(command, json_decode = True, printOutput = True):\n", + " print(command)\n", + " process = subprocess.Popen(command.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)\n", + " output, error = process.communicate()\n", + " if process.returncode != 0: \n", + " print(\"Process failed %d \\n%s\" % (process.returncode, error.decode(\"utf-8\")))\n", + " raise Exception()\n", + " if output:\n", + " output = output.decode(\"utf-8\")\n", + " if printOutput:\n", + " print(output)\n", + " try:\n", + " return json.loads(output)\n", + " except:\n", + " return output\r\n", + "\r\n", + "def get_random_string(length):\r\n", + " letters = string.ascii_lowercase\r\n", + " result_str = ''.join(random.choice(letters) for i in range(length))\r\n", + " print(\"Random string of length\", length, \"is:\", result_str)\r\n", + " return result_str" + ], + "metadata": { + "azdata_cell_guid": "dd0caed8-b12e-4a9e-9705-fdcf5fa3ff7e", + "tags": [] + }, + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Connecting to your Azure account\r\n", + "" + ], + "metadata": { + "azdata_cell_guid": "b06fee5e-355d-47fc-8c1f-41294756cc87" + } + }, + { + "cell_type": "code", + "source": [ + "subscriptions = run_command('az account list', printOutput = False)\r\n", + "if azure_sqlvm_nb_var_subscription not in (subscription[\"id\"] for subscription in subscriptions):\r\n", + " run_command('az login')" + ], + "metadata": { + "azdata_cell_guid": "2b35980c-bb65-4ba7-95fd-7f73d8f785b5" + }, + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Setting your Azure subscription\r\n", + "" + ], + "metadata": { + "azdata_cell_guid": "b58f1048-3e9d-4888-bda0-4d0443a11c97" + } + }, + { + "cell_type": "code", + "source": [ + "run_command(\r\n", + " 'az account set '\r\n", + " '--subscription {0}'\r\n", + " .format(\r\n", + " azure_sqlvm_nb_var_subscription));" + ], + "metadata": { + "azdata_cell_guid": "0cc44e68-3810-46f4-b29c-e6ad4321e384" + }, + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Configure network settings\r\n", + "All networking configurations are handled in this step, including virtual network, subnet, public IP address, network security group, connectivity settings, and network interface.\r\n", + "\r\n", + "1. If you selected the option to create a new virtual network, subnet, or a public IP address, they will be created here. These resources are used to provide network connectivity to the virtual machine and connect it to the internet." + ], + "metadata": { + "azdata_cell_guid": "202634eb-7edf-4ff4-8486-fffbda45dbc8" + } + }, + { + "cell_type": "code", + "source": [ + "subnet_name = azure_sqlvm_subnet\r\n", + "vnet_name = azure_sqlvm_virtnet\r\n", + "pip_name = azure_sqlvm_publicip\r\n", + "\r\n", + "\r\n", + "if azure_sqlvm_newVirtualNetwork:\r\n", + " run_command(\r\n", + " 'az network vnet create '\r\n", + " '--resource-group {0} '\r\n", + " '--name {1} '\r\n", + " '--location {2} '\r\n", + " '--address-prefixes 192.168.0.0/16 '\r\n", + " .format(\r\n", + " azure_sqlvm_nb_var_resource_group_name, \r\n", + " vnet_name, \r\n", + " azure_sqlvm_location));\r\n", + "\r\n", + "if azure_sqlvm_newSubnet:\r\n", + " run_command(\r\n", + " 'az network vnet subnet create '\r\n", + " '--name {0} '\r\n", + " '--resource-group {1} '\r\n", + " '--vnet-name {2} '\r\n", + " '--address-prefixes 192.168.1.0/24'\r\n", + " .format(\r\n", + " subnet_name,\r\n", + " azure_sqlvm_nb_var_resource_group_name,\r\n", + " vnet_name\r\n", + " ));\r\n", + "\r\n", + "if azure_sqlvm_newPublicIp:\r\n", + " run_command(\r\n", + " 'az network public-ip create '\r\n", + " '--resource-group {0} '\r\n", + " '--location {1} '\r\n", + " '--allocation-method Static '\r\n", + " '--idle-timeout 4 '\r\n", + " '--name {2}'\r\n", + " .format(\r\n", + " azure_sqlvm_nb_var_resource_group_name, \r\n", + " azure_sqlvm_location, \r\n", + " pip_name));" + ], + "metadata": { + "azdata_cell_guid": "af88cdae-1a62-4990-9231-094481c9337d" + }, + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "2. Create a network security group and configure rules to allow remote desktop (RDP) and SQL Server connections." + ], + "metadata": { + "azdata_cell_guid": "3b25e16e-b150-4a2e-80dc-66f2d18b43fb" + } + }, + { + "cell_type": "code", + "source": [ + "nsg_name = azure_sqlvm_nb_var_resource_group_name + 'nsg'\r\n", + "nsg_name_id = run_command(\r\n", + " 'az network nsg create '\r\n", + " '--resource-group {0} '\r\n", + " '--location {1} '\r\n", + " '--name {2} '\r\n", + " .format(\r\n", + " azure_sqlvm_nb_var_resource_group_name,\r\n", + " azure_sqlvm_location, \r\n", + " nsg_name));\r\n", + "\r\n", + "if azure_sqlvm_allow_rdp:\r\n", + " run_command(\r\n", + " 'az network nsg rule create '\r\n", + " '--name RDPRule '\r\n", + " '--nsg-name {0} '\r\n", + " '--priority 1000 '\r\n", + " '--resource-group {1} '\r\n", + " '--protocol Tcp '\r\n", + " '--direction Inbound '\r\n", + " '--source-address-prefixes * '\r\n", + " '--source-port-range * '\r\n", + " '--destination-address-prefixes * '\r\n", + " '--destination-port-range 3389 '\r\n", + " '--access Allow'\r\n", + " .format(\r\n", + " nsg_name,\r\n", + " azure_sqlvm_nb_var_resource_group_name));" + ], + "metadata": { + "azdata_cell_guid": "debe940d-0d0f-4540-be5b-4d6495d338e1" + }, + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "3. Create the network interface." + ], + "metadata": { + "azdata_cell_guid": "d44de03c-d4f2-48ef-8a60-507069d6c08e" + } + }, + { + "cell_type": "code", + "source": [ + "interface_name = azure_sqlvm_nb_var_resource_group_name + \"int\"\r\n", + "\r\n", + "command = (\r\n", + " 'az network nic create '\r\n", + " '--name {0} '\r\n", + " '--resource-group {1} '\r\n", + " '--subnet {2} '\r\n", + " '--public-ip-address {3} '\r\n", + " '--network-security-group {4} '\r\n", + " '--location {5} '\r\n", + ")\r\n", + "\r\n", + "if azure_sqlvm_newSubnet:\r\n", + " command += '--vnet-name {6} '\r\n", + "\r\n", + "run_command(\r\n", + " command\r\n", + " .format(\r\n", + " interface_name,\r\n", + " azure_sqlvm_nb_var_resource_group_name,\r\n", + " subnet_name,\r\n", + " pip_name,\r\n", + " nsg_name,\r\n", + " azure_sqlvm_location,\r\n", + " vnet_name));" + ], + "metadata": { + "azdata_cell_guid": "6dbb3ea0-b52f-4ed2-bd24-59096d134e88" + }, + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Create Azure Virtual Machine and Azure SQL VM resources\r\n", + "First the Azure VM will be created based on all the settings previously specified. Next the SQL VM will be created to include a default set of SQL connectivity settings. The SQL VM resource is where you can manage any SQL Server manageability features offered by Azure. [Learn more](https://docs.microsoft.com/azure/azure-sql/virtual-machines/windows/sql-server-on-azure-vm-iaas-what-is-overview) about what you can do with your Azure SQL VM after it has been created." + ], + "metadata": { + "azdata_cell_guid": "c42ec570-331a-46ea-b358-b05e47320967" + } + }, + { + "cell_type": "code", + "source": [ + "# Create the VM\r\n", + "azure_sqlvm_image = 'sql2019-ws2019'\r\n", + "\r\n", + "run_command(\r\n", + " 'az vm create '\r\n", + " '--name {0} '\r\n", + " '--size {1} '\r\n", + " '--computer-name {0} '\r\n", + " '--admin-username {2} '\r\n", + " '--admin-password {3} '\r\n", + " '--image {4}:{5}:{6}:{7} '\r\n", + " '--nics {8} '\r\n", + " '--resource-group {9} '\r\n", + " '--location {10} '\r\n", + " .format(\r\n", + " azure_sqlvm_vmname,\r\n", + " azure_sqlvm_vmsize,\r\n", + " azure_sqlvm_username,\r\n", + " azure_sqlvm_password,\r\n", + " 'MicrosoftSQLServer',\r\n", + " azure_sqlvm_image,\r\n", + " azure_sqlvm_image_sku,\r\n", + " 'latest',\r\n", + " interface_name,\r\n", + " azure_sqlvm_nb_var_resource_group_name,\r\n", + " azure_sqlvm_location\r\n", + " )\r\n", + ");" + ], + "metadata": { + "azdata_cell_guid": "05fa1f3d-94e1-480f-ad20-d3006bafc6ac", + "tags": [] + }, + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "source": [ + "command = (\r\n", + " 'az sql vm create '\r\n", + " '--name {0} '\r\n", + " '--license-type PAYG '\r\n", + " '--resource-group {1} '\r\n", + " '--connectivity-type {2} '\r\n", + " '--sql-mgmt-type Full '\r\n", + " '--location {3} '\r\n", + ")\r\n", + "\r\n", + "if azure_sqlvm_enableSqlAuthentication:\r\n", + " command += '--sql-auth-update-username {4} '\r\n", + " command += '--sql-auth-update-pwd {5} '\r\n", + "\r\n", + "if azure_sqlvm_sqlConnectivityType != 'local':\r\n", + " command += '--port {6} '\r\n", + "\r\n", + "run_command(\r\n", + " command\r\n", + " .format(\r\n", + " azure_sqlvm_vmname,\r\n", + " azure_sqlvm_nb_var_resource_group_name,\r\n", + " azure_sqlvm_sqlConnectivityType,\r\n", + " azure_sqlvm_location,\r\n", + " azure_sqlvm_sqlAuthenticationUsername,\r\n", + " azure_sqlvm_sqlAuthenticationPassword,\r\n", + " azure_sqlvm_port,\r\n", + " )\r\n", + ");" + ], + "metadata": { + "azdata_cell_guid": "bb3b5436-c34b-44b3-b631-ea60c9dcf16a" + }, + "outputs": [], + "execution_count": null + } + ] +} \ No newline at end of file diff --git a/extensions/resource-deployment/package.json b/extensions/resource-deployment/package.json index 0fb35aa32a..ffc3ead921 100644 --- a/extensions/resource-deployment/package.json +++ b/extensions/resource-deployment/package.json @@ -456,6 +456,46 @@ } ] } + }, + { + "name": "azure-sql-vm", + "displayName": "%azure-sqlvm-display-name%", + "description": "%azure-sqlvm-description%", + "platforms": "*", + "icon": { + "light": "./images/azure-sql-vm.svg", + "dark": "./images/azure-sql-vm.svg" + }, + "providers": [ + { + "azureSQLVMWizard":{ + "notebook": "./notebooks/azurevm/create-sqlvm.ipynb" + }, + "requiredTools": [ + { + "name": "azure-cli" + } + ], + "when": true + } + ], + "agreement": { + "template": "%azure-sqlvm-agreement%", + "links": [ + { + "text": "%microsoft-privacy-statement%", + "url": "https://go.microsoft.com/fwlink/?LinkId=853010" + }, + { + "text": "%azure-sqlvm-agreement-sqlvm-eula%", + "url": "https://azure.microsoft.com/support/legal/" + }, + { + "text": "%azure-sqlvm-agreement-azdata-eula%", + "url": "https://aka.ms/eula-azdata-en" + } + ] + } } ] }, diff --git a/extensions/resource-deployment/package.nls.json b/extensions/resource-deployment/package.nls.json index 87264d27d9..63d6069bd9 100644 --- a/extensions/resource-deployment/package.nls.json +++ b/extensions/resource-deployment/package.nls.json @@ -47,6 +47,29 @@ "bdc-agreement-bdc-eula": "SQL Server License Terms", "deployment.configuration.title": "Deployment configuration", "azdata-install-location-description": "Location of the azdata package used for the install command", + "azure-sqlvm-display-name": "SQL Server on Azure Virtual Machine", + "azure-sqlvm-description": "Create SQL virtual machines on Azure. Best for migrations and applications requiring OS-level access.", + "azure-sqlvm-deploy-dialog-title": "Deploy Azure SQL virtual machine", + "azure-sqlvm-deploy-dialog-action-text": "Script to notebook", + "azure-sqlvm-agreement": "I accept {0}, {1} and {2}.", + "azure-sqlvm-agreement-sqlvm-eula": "Azure SQL VM License Terms", + "azure-sqlvm-agreement-azdata-eula": "azdata License Terms", + "azure-sqlvm-azure-account-page-label":"Azure information", + "azure-sqlvm-azure-location-label": "Azure locations", + "azure-sqlvm-vm-information-page-label": "VM information", + "azure-sqlvm-image-label": "Image", + "azure-sqlvm-image-sku-label": "VM image SKU", + "azure-sqlvm-publisher-label": "Publisher", + "azure-sqlvm-vmname-label": "Virtual machine name", + "azure-sqlvm-vmsize-label": "Size", + "azure-sqlvm-storage-page-lable": "Storage account", + "azure-sqlvm-storage-accountname-label": "Storage account name", + "azure-sqlvm-storage-sku-label": "Storage account SKU type", + "azure-sqlvm-vm-administrator-account-page-label": "Administrator account", + "azure-sqlvm-username-label": "Username", + "azure-sqlvm-password-label": "Password", + "azure-sqlvm-password-confirm-label": "Confirm password", + "azure-sqlvm-vm-summary-page-label": "Summary", "azure-sqldb-display-name": "Azure SQL Database", "azure-sqldb-description": "Select a resource type and then you will be taken to the Azure portal to create the Azure resource.", "azure-sqldb-ok-button-text": "Create in Azure portal", diff --git a/extensions/resource-deployment/src/interfaces.ts b/extensions/resource-deployment/src/interfaces.ts index c8d522cd45..a25de03b9c 100644 --- a/extensions/resource-deployment/src/interfaces.ts +++ b/extensions/resource-deployment/src/interfaces.ts @@ -67,6 +67,10 @@ export interface CommandDeploymentProvider extends DeploymentProviderBase { command: string; } +export interface AzureSQLVMDeploymentProvider extends DeploymentProviderBase { + azureSQLVMWizard: AzureSQLVMWizardInfo; +} + export function instanceOfDialogDeploymentProvider(obj: any): obj is DialogDeploymentProvider { return obj && 'dialog' in obj; } @@ -95,12 +99,16 @@ export function instanceOfCommandDeploymentProvider(obj: any): obj is CommandDep return obj && 'command' in obj; } +export function instanceOfAzureSQLVMDeploymentProvider(obj: any): obj is AzureSQLVMDeploymentProvider { + return obj && 'azureSQLVMWizard' in obj; +} + export interface DeploymentProviderBase { requiredTools: ToolRequirementInfo[]; when: string; } -export type DeploymentProvider = DialogDeploymentProvider | BdcWizardDeploymentProvider | NotebookWizardDeploymentProvider | NotebookDeploymentProvider | WebPageDeploymentProvider | DownloadDeploymentProvider | CommandDeploymentProvider; +export type DeploymentProvider = DialogDeploymentProvider | BdcWizardDeploymentProvider | NotebookWizardDeploymentProvider | NotebookDeploymentProvider | WebPageDeploymentProvider | DownloadDeploymentProvider | CommandDeploymentProvider | AzureSQLVMDeploymentProvider; export interface BdcWizardInfo { notebook: string | NotebookPathInfo; @@ -136,6 +144,10 @@ export interface CommandBasedDialogInfo extends DialogInfoBase { command: string; } +export interface AzureSQLVMWizardInfo { + notebook: string | NotebookPathInfo; +} + export type DialogInfo = NotebookBasedDialogInfo | CommandBasedDialogInfo; export function instanceOfNotebookBasedDialogInfo(obj: any): obj is NotebookBasedDialogInfo { @@ -238,6 +250,8 @@ export interface FieldInfo extends SubFieldInfo, FieldInfoBase { editable?: boolean; // for editable drop-down, enabled?: boolean; isEvaluated?: boolean; + valueLookup?: string; // for fetching dropdown options + validationLookup?: string // for fetching text field validations } export interface KubeClusterContextFieldInfo extends FieldInfo { diff --git a/extensions/resource-deployment/src/services/resourceTypeService.ts b/extensions/resource-deployment/src/services/resourceTypeService.ts index ff7c0ea41a..f6be8dca6d 100644 --- a/extensions/resource-deployment/src/services/resourceTypeService.ts +++ b/extensions/resource-deployment/src/services/resourceTypeService.ts @@ -13,13 +13,14 @@ import * as nls from 'vscode-nls'; import { INotebookService } from './notebookService'; import { IPlatformService } from './platformService'; import { IToolsService } from './toolsService'; -import { ResourceType, ResourceTypeOption, NotebookPathInfo, DeploymentProvider, instanceOfWizardDeploymentProvider, instanceOfDialogDeploymentProvider, instanceOfNotebookDeploymentProvider, instanceOfDownloadDeploymentProvider, instanceOfWebPageDeploymentProvider, instanceOfCommandDeploymentProvider, instanceOfNotebookBasedDialogInfo, instanceOfNotebookWizardDeploymentProvider, NotebookInfo } from '../interfaces'; +import { ResourceType, ResourceTypeOption, NotebookPathInfo, DeploymentProvider, instanceOfWizardDeploymentProvider, instanceOfDialogDeploymentProvider, instanceOfNotebookDeploymentProvider, instanceOfDownloadDeploymentProvider, instanceOfWebPageDeploymentProvider, instanceOfCommandDeploymentProvider, instanceOfNotebookBasedDialogInfo, instanceOfNotebookWizardDeploymentProvider, NotebookInfo, instanceOfAzureSQLVMDeploymentProvider } from '../interfaces'; import { DeployClusterWizard } from '../ui/deployClusterWizard/deployClusterWizard'; import { DeploymentInputDialog } from '../ui/deploymentInputDialog'; import { KubeService } from './kubeService'; import { AzdataService } from './azdataService'; import { NotebookWizard } from '../ui/notebookWizard/notebookWizard'; +import { DeployAzureSQLVMWizard } from '../ui/deployAzureSQLVMWizard/deployAzureSQLVMWizard'; const localize = nls.loadMessageBundle(); export interface IResourceTypeService { @@ -74,6 +75,9 @@ export class ResourceTypeService implements IResourceTypeService { else if ('notebookWizard' in provider) { this.updateNotebookPath(provider.notebookWizard, extensionPath); } + else if ('azureSQLVMWizard' in provider) { + this.updateNotebookPath(provider.azureSQLVMWizard, extensionPath); + } }); } @@ -182,7 +186,8 @@ export class ResourceTypeService implements IResourceTypeService { && !instanceOfNotebookDeploymentProvider(provider) && !instanceOfDownloadDeploymentProvider(provider) && !instanceOfWebPageDeploymentProvider(provider) - && !instanceOfCommandDeploymentProvider(provider)) { + && !instanceOfCommandDeploymentProvider(provider) + && !instanceOfAzureSQLVMDeploymentProvider(provider)) { errorMessages.push(`No deployment method defined for the provider, ${providerPositionInfo}`); } @@ -275,6 +280,9 @@ export class ResourceTypeService implements IResourceTypeService { vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(provider.webPageUrl)); } else if (instanceOfCommandDeploymentProvider(provider)) { vscode.commands.executeCommand(provider.command); + } else if (instanceOfAzureSQLVMDeploymentProvider(provider)) { + const wizard = new DeployAzureSQLVMWizard(provider.azureSQLVMWizard, this.notebookService, this.toolsService); + wizard.open(); } } diff --git a/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/constants.ts b/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/constants.ts new file mode 100644 index 0000000000..ac443f70e2 --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/constants.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +export const standardWidth: string = '480px'; + +// Deploy Azure SQL VM wizard constants +export const WizardTitle = localize('deployAzureSQLVM.NewSQLVMTitle', "Deploy Azure SQL VM"); +export const WizardDoneButtonLabel = localize('deployAzureSQLVM.ScriptToNotebook', "Script to Notebook"); +export const MissingRequiredInformationErrorMessage = localize('deployAzureSQLVM.MissingRequiredInfoError', "Please fill out the required fields marked with red asterisks."); + +// Azure settings page constants +export const AzureSettingsPageTitle = localize('deployAzureSQLVM.AzureSettingsPageTitle', "Azure settings"); +export const AzureAccountDropdownLabel = localize('deployAzureSQLVM.AzureAccountDropdownLabel', "Azure Account"); +export const AzureAccountSubscriptionDropdownLabel = localize('deployAzureSQLVM.AzureSubscriptionDropdownLabel', "Subscription"); +export const AzureAccountResourceGroupDropdownLabel = localize('deployAzureSQLVM.ResourceGroup', "Resource Group"); +export const AzureAccountRegionDropdownLabel = localize('deployAzureSQLVM.AzureRegionDropdownLabel', "Region"); + +// VM settings page constants +export const VmSettingsPageTitle = localize('deployeAzureSQLVM.VmSettingsPageTitle', "Virtual machine settings"); +export const VmNameTextBoxLabel = localize('deployAzureSQLVM.VmNameTextBoxLabel', "Virtual machine name"); +export const VmAdminUsernameTextBoxLabel = localize('deployAzureSQLVM.VmAdminUsernameTextBoxLabel', "Administrator account username"); +export const VmAdminPasswordTextBoxLabel = localize('deployAzureSQLVM.VmAdminPasswordTextBoxLabel', "Administrator account password"); +export const VmAdminConfirmPasswordTextBoxLabel = localize('deployAzureSQLVM.VmAdminConfirmPasswordTextBoxLabel', "Confirm password"); +export const VmImageDropdownLabel = localize('deployAzureSQLVM.VmImageDropdownLabel', "Image"); +export const VmSkuDropdownLabel = localize('deployAzureSQLVM.VmSkuDropdownLabel', "Image SKU"); +export const VmVersionDropdownLabel = localize('deployAzureSQLVM.VmImageVersionDropdownLabel', "Image Version"); +export const VmSizeDropdownLabel = localize('deployAzureSQLVM.VmSizeDropdownLabel', "Size"); +export const VmSizeLearnMoreLabel = localize('deployeAzureSQLVM.VmSizeLearnMoreLabel', "Click here to learn more about pricing and supported VM sizes"); + +// Network settings page constants +export const NetworkSettingsPageTitle = localize('deployAzureSQLVM.NetworkSettingsPageTitle', "Networking"); +export const NetworkSettingsPageDescription = localize('deployAzureSQLVM.NetworkSettingsPageDescription', "Configure network settings"); +export const NetworkSettingsNewVirtualNetwork = localize('deployAzureSQLVM.NetworkSettingsNewVirtualNetwork', "New virtual network"); +export const VirtualNetworkDropdownLabel = localize('deployAzureSQLVM.VirtualNetworkDropdownLabel', "Virtual Network"); +export const NetworkSettingsNewSubnet = localize('deployAzureSQLVM.NetworkSettingsNewSubnet', "New subnet"); +export const SubnetDropdownLabel = localize('deployAzureSQLVM.SubnetDropdownLabel', "Subnet"); +export const PublicIPDropdownLabel = localize('deployAzureSQLVM.PublicIPDropdownLabel', "Public IP"); +export const NetworkSettingsNewPublicIp = localize('deployAzureSQLVM.NetworkSettingsUseExistingPublicIp', "New public ip"); +export const RDPAllowCheckboxLabel = localize('deployAzureSQLVM.VmRDPAllowCheckboxLabel', "Enable Remote Desktop (RDP) inbound port (3389)"); + +// SQL Server settings page constants +export const SqlServerSettingsPageTitle = localize('deployAzureSQLVM.SqlServerSettingsPageTitle', "SQL Servers settings"); +export const SqlConnectivityTypeDropdownLabel = localize('deployAzureSQLVM.SqlConnectivityTypeDropdownLabel', "SQL connectivity"); +export const SqlPortLabel = localize('deployAzureSQLVM.SqlPortLabel', "Port"); +export const SqlEnableSQLAuthenticationLabel = localize('deployAzureSQLVM.SqlEnableSQLAuthenticationLabel', "Enable SQL authentication"); +export const SqlAuthenticationUsernameLabel = localize('deployAzureSQLVM.SqlAuthenticationUsernameLabel', "Username"); +export const SqlAuthenticationPasswordLabel = localize('deployAzureSQLVM.SqlAuthenticationPasswordLabel', "Password"); +export const SqlAuthenticationConfirmPasswordLabel = localize('deployAzureSQLVM.SqlAuthenticationConfirmPasswordLabel', "Confirm password"); diff --git a/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/deployAzureSQLVMWizard.ts b/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/deployAzureSQLVMWizard.ts new file mode 100644 index 0000000000..749d9ccad3 --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/deployAzureSQLVMWizard.ts @@ -0,0 +1,223 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as vscode from 'vscode'; +import * as constants from './constants'; +import { INotebookService } from '../../services/notebookService'; +import { IToolsService } from '../../services/toolsService'; +import { WizardBase } from '../wizardBase'; +import { WizardPageBase } from '../wizardPageBase'; +import { DeployAzureSQLVMWizardModel } from './deployAzureSQLVMWizardModel'; +import { AzureSQLVMWizardInfo } from '../../interfaces'; +import { AzureSettingsPage } from './pages/azureSettingsPage'; +import { VmSettingsPage } from './pages/vmSettingsPage'; +import axios, { AxiosRequestConfig } from 'axios'; +import { NetworkSettingsPage } from './pages/networkSettingsPage'; +import { SqlServerSettingsPage } from './pages/sqlServerSettingsPage'; +import { AzureSQLVMSummaryPage } from './pages/summaryPage'; +import { EOL } from 'os'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +export class DeployAzureSQLVMWizard extends WizardBase, DeployAzureSQLVMWizardModel> { + private cache: Map = new Map(); + + constructor(private wizardInfo: AzureSQLVMWizardInfo, private _notebookService: INotebookService, private _toolsService: IToolsService) { + super( + constants.WizardTitle, + new DeployAzureSQLVMWizardModel(), + _toolsService + ); + } + + protected initialize(): void { + this.setPages(this.getPages()); + this.wizardObject.generateScriptButton.hidden = true; + this.wizardObject.doneButton.label = constants.WizardDoneButtonLabel; + } + + + public get notebookService(): INotebookService { + return this._notebookService; + } + + public get toolService(): IToolsService { + return this._toolsService; + } + + + + protected async onOk(): Promise { + await this.scriptToNotebook(); + } + + protected onCancel(): void { + } + + private getPages(): WizardPageBase[] { + const pages: WizardPageBase[] = []; + pages.push(new AzureSettingsPage(this)); + pages.push(new VmSettingsPage(this)); + pages.push(new NetworkSettingsPage(this)); + pages.push(new SqlServerSettingsPage(this)); + pages.push(new AzureSQLVMSummaryPage(this)); + return pages; + } + + private async scriptToNotebook(): Promise { + this.setEnvironmentVariables(process.env); + const variableValueStatements = this.model.getCodeCellContentForNotebook(); + const insertionPosition = 2; // Cell number 5 is the position where the python variable setting statements need to be inserted in this.wizardInfo.notebook. + try { + await this.notebookService.launchNotebookWithEdits(this.wizardInfo.notebook, variableValueStatements, insertionPosition); + } catch (error) { + vscode.window.showErrorMessage(error); + } + } + + private setEnvironmentVariables(env: NodeJS.ProcessEnv): void { + env['AZDATA_NB_VAR_AZURE_SQLVM_PASSWORD'] = this.model.vmPassword; + env['AZDATA_NB_VAR_AZURE_SQLVM_SQL_PASSWORD'] = this.model.sqlAuthenticationPassword; + } + + public async getRequest(url: string, useCache = false): Promise { + if (useCache) { + if (this.cache.has(url)) { + return this.cache.get(url); + } + } + let token = this.model.securityToken.token; + const config: AxiosRequestConfig = { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + validateStatus: () => true // Never throw + }; + const response = await axios.get(url, config); + if (response.status !== 200) { + let errorMessage: string[] = []; + errorMessage.push(response.status.toString()); + errorMessage.push(response.statusText); + if (response.data && response.data.error) { + errorMessage.push(`${response.data.error.code} : ${response.data.error.message}`); + } + vscode.window.showErrorMessage(errorMessage.join(EOL)); + } + if (useCache) { + this.cache.set(url, response); + } + return response; + } + + public createFormRowComponent(view: azdata.ModelView, title: string, description: string, component: azdata.Component, required: boolean): azdata.FlexContainer { + + component.updateProperties({ + required: required, + width: '480px' + }); + + const labelText = view.modelBuilder.text() + .withProperties( + { + value: title, + width: '250px', + description: description, + requiredIndicator: required, + }) + .component(); + + labelText.updateCssStyles({ + 'font-weight': '400', + 'font-size': '13px', + }); + + const flexContainer = view.modelBuilder.flexContainer() + .withLayout( + { + flexFlow: 'row', + alignItems: 'center', + }) + .withItems( + [labelText, component], + { + CSSStyles: { 'margin-right': '5px' } + }) + .component(); + return flexContainer; + } + + public changeComponentDisplay(component: azdata.Component, display: ('none' | 'block')) { + component.updateProperties({ + required: display === 'block' + }); + component.updateCssStyles({ + display: display + }); + } + + public changeRowDisplay(container: azdata.FlexContainer, display: ('none' | 'block')) { + container.items.map((component) => { + component.updateProperties({ + required: (display === 'block'), + }); + component.updateCssStyles({ + display: display, + }); + }); + } + + public addDropdownValues(component: azdata.DropDownComponent, values: azdata.CategoryValue[], width?: number) { + component.updateProperties({ + values: values, + width: '480px' + }); + } + + public showErrorMessage(message: string) { + this.wizardObject.message = { + text: message, + level: azdata.window.MessageLevel.Error + }; + } + + public validatePassword(password: string): string[] { + /** + * 1. Password length should be between 12 and 123. + * 2. Password must have 3 of the following: 1 lower case character, 1 upper case character, 1 number, and 1 special character. + */ + + let errorMessages = []; + + if (password.length < 12 || password.length > 123) { + errorMessages.push(localize('sqlVMDeploymentWizard.PasswordLengthError', "Password must be between 12 and 123 characters long.")); + } + + let charTypeCounter = 0; + + if (new RegExp('.*[a-z].*').test(password)) { + charTypeCounter++; + } + + if (new RegExp('.*[A-Z].*').test(password)) { + charTypeCounter++; + } + + if (new RegExp('.*[0-9].*').test(password)) { + charTypeCounter++; + } + + if (/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+/.test(password)) { + charTypeCounter++; + } + + if (charTypeCounter < 3) { + errorMessages.push(localize('sqlVMDeploymentWizard.PasswordSpecialCharRequirementError', "Password must have 3 of the following: 1 lower case character, 1 upper case character, 1 number, and 1 special character.")); + } + + return errorMessages; + } +} diff --git a/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/deployAzureSQLVMWizardModel.ts b/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/deployAzureSQLVMWizardModel.ts new file mode 100644 index 0000000000..ccc2054ade --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/deployAzureSQLVMWizardModel.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { EOL } from 'os'; +import * as azdata from 'azdata'; +import { Model } from '../model'; + +export class DeployAzureSQLVMWizardModel extends Model { + public azureAccount!: azdata.Account; + public securityToken!: any; + public azureSubscription!: string; + public azureSubscriptionDisplayName!: string; + public azureResouceGroup!: string; + public azureRegion!: string; + + public vmName!: string; + public vmUsername!: string; + public vmPassword!: string; + public vmImage!: string; + public vmImageSKU!: string; + public vmImageVersion!: string; + public vmSize!: string; + + public virtualNetworkName!: string; + public newVirtualNetwork!: 'True' | 'False'; + public subnetName!: string; + public newSubnet!: 'True' | 'False'; + public publicIpName!: string; + public newPublicIp!: 'True' | 'False'; + public allowRDP!: 'True' | 'False'; + + public sqlConnectivityType!: string; + public port!: number; + public enableSqlAuthentication!: string; + public sqlAuthenticationUsername!: string; + public sqlAuthenticationPassword!: string; + public sqlOptimizationDropdown!: string; + + + constructor() { + super(); + } + + public getCodeCellContentForNotebook(): string[] { + + const statements: string[] = []; + + statements.push('import os'); + statements.push(`azure_sqlvm_nb_var_subscription = '${this.azureSubscription}'`); + statements.push(`azure_sqlvm_nb_var_resource_group_name = '${this.azureResouceGroup}'`); + statements.push(`azure_sqlvm_location = '${this.azureRegion}'`); + statements.push(`azure_sqlvm_vmname = '${this.vmName}'`); + statements.push(`azure_sqlvm_username = '${this.vmUsername}'`); + statements.push(`azure_sqlvm_image = '${this.vmImage}'`); + statements.push(`azure_sqlvm_image_sku = '${this.vmImageSKU}'`); + statements.push(`azure_sqlvm_image_version = '${this.vmImageVersion}'`); + statements.push(`azure_sqlvm_vmsize = '${this.vmSize}'`); + statements.push(`azure_sqlvm_newVirtualNetwork = ${this.newVirtualNetwork}`); + statements.push(`azure_sqlvm_virtnet = '${this.virtualNetworkName}'`); + statements.push(`azure_sqlvm_newSubnet = ${this.newSubnet}`); + statements.push(`azure_sqlvm_subnet = '${this.subnetName}'`); + statements.push(`azure_sqlvm_newPublicIp = ${this.newPublicIp}`); + statements.push(`azure_sqlvm_publicip = '${this.publicIpName}'`); + statements.push(`azure_sqlvm_allow_rdp = ${this.allowRDP}`); + statements.push(`azure_sqlvm_sqlConnectivityType = '${this.sqlConnectivityType}'`); + statements.push(`azure_sqlvm_port = '${this.port}'`); + statements.push(`azure_sqlvm_enableSqlAuthentication = ${this.enableSqlAuthentication}`); + statements.push(`azure_sqlvm_sqlAuthenticationUsername = '${this.sqlAuthenticationUsername}'`); + + return statements.map(line => line + EOL); + } +} diff --git a/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/pages/azureSettingsPage.ts b/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/pages/azureSettingsPage.ts new file mode 100644 index 0000000000..12f68a1435 --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/pages/azureSettingsPage.ts @@ -0,0 +1,302 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as constants from '../constants'; +import { WizardPageBase } from '../../wizardPageBase'; +import { DeployAzureSQLVMWizard } from '../deployAzureSQLVMWizard'; +import { apiService } from '../../../services/apiService'; +import { azureResource } from 'azureResource'; +import * as vscode from 'vscode'; + +export class AzureSettingsPage extends WizardPageBase { + // <- means depends on + //dropdown for azure accounts + private _azureAccountsDropdown!: azdata.DropDownComponent; + private signInButton!: azdata.ButtonComponent; + private refreshButton!: azdata.ButtonComponent; + + private buttonFlexContainer!: azdata.FlexContainer; + + //dropdown for subscription accounts <- azure account dropdown + private _azureSubscriptionsDropdown!: azdata.DropDownComponent; + + //dropdown for resource groups <- subscription dropdown + private _resourceGroupDropdown!: azdata.DropDownComponent; + + //dropdown for azure regions <- subscription dropdown + private _azureRegionsDropdown!: azdata.DropDownComponent; + + private _form!: azdata.FormContainer; + + private _accountsMap!: Map; + private _subscriptionsMap!: Map; + constructor(wizard: DeployAzureSQLVMWizard) { + super( + constants.AzureSettingsPageTitle, + '', + wizard + ); + this._accountsMap = new Map(); + this._subscriptionsMap = new Map(); + } + + public async initialize() { + this.pageObject.registerContent(async (view: azdata.ModelView) => { + + await Promise.all([ + this.createAzureAccountsDropdown(view), + this.createAzureSubscriptionsDropdown(view), + this.createResourceDropdown(view), + this.createAzureRegionsDropdown(view) + ]); + this.populateAzureAccountsDropdown(); + + this._form = view.modelBuilder.formContainer() + .withFormItems( + [ + { + component: this.wizard.createFormRowComponent(view, constants.AzureAccountDropdownLabel, '', this._azureAccountsDropdown, true) + }, + { + component: this.buttonFlexContainer + }, + { + component: this.wizard.createFormRowComponent(view, constants.AzureAccountSubscriptionDropdownLabel, '', this._azureSubscriptionsDropdown, true) + }, + { + component: this.wizard.createFormRowComponent(view, constants.AzureAccountResourceGroupDropdownLabel, '', this._resourceGroupDropdown, true) + }, + { + component: this.wizard.createFormRowComponent(view, constants.AzureAccountRegionDropdownLabel, '', this._azureRegionsDropdown, true) + } + ], + { + horizontal: false, + componentWidth: '100%' + }) + .withLayout({ width: '100%' }) + .component(); + return view.initializeModel(this._form); + }); + } + + public async onEnter(): Promise { + this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { + return true; + }); + } + + public async onLeave(): Promise { + this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { + return true; + }); + } + + private async createAzureAccountsDropdown(view: azdata.ModelView) { + + this._azureAccountsDropdown = view.modelBuilder.dropDown().withProperties({}).component(); + + this._azureAccountsDropdown.onValueChanged(async (value) => { + this.wizard.model.azureAccount = this._accountsMap.get(value.selected)!; + this.populateAzureSubscriptionsDropdown(); + }); + + this.signInButton = view.modelBuilder.button().withProperties({ + label: 'Sign In', + width: '100px' + }).component(); + this.refreshButton = view.modelBuilder.button().withProperties({ + label: 'Refresh', + width: '100px' + }).component(); + + this.signInButton.onDidClick(async (event) => { + await vscode.commands.executeCommand('workbench.actions.modal.linkedAccount'); + await this.populateAzureAccountsDropdown(); + }); + this.refreshButton.onDidClick(async (event) => { + await this.populateAzureAccountsDropdown(); + }); + this.buttonFlexContainer = view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'row' + }).withItems([this.signInButton, this.refreshButton], { CSSStyles: { 'margin-right': '5px', } }).component(); + + } + + private async populateAzureAccountsDropdown() { + this._azureAccountsDropdown.loading = true; + let accounts = await azdata.accounts.getAllAccounts(); + + if (accounts.length === 0) { + this.wizard.showErrorMessage('Sign in to an Azure account first'); + return; + } else { + this.wizard.showErrorMessage(''); + } + + this.wizard.addDropdownValues( + this._azureAccountsDropdown, + accounts.map((account): azdata.CategoryValue => { + let accountCategoryValue = { + displayName: account.displayInfo.displayName, + name: account.displayInfo.displayName + }; + this._accountsMap.set(accountCategoryValue.displayName, account); + return accountCategoryValue; + }), + ); + + this.wizard.model.azureAccount = accounts[0]; + this._azureAccountsDropdown.loading = false; + + await this.populateAzureSubscriptionsDropdown(); + } + + private async createAzureSubscriptionsDropdown(view: azdata.ModelView) { + this._azureSubscriptionsDropdown = view.modelBuilder.dropDown().withProperties({}).component(); + + this._azureSubscriptionsDropdown.onValueChanged(async (value) => { + + let currentSubscriptionValue = this._azureSubscriptionsDropdown.value as azdata.CategoryValue; + this.wizard.model.azureSubscription = currentSubscriptionValue.name; + this.wizard.model.azureSubscriptionDisplayName = currentSubscriptionValue.displayName; + + this.wizard.model.securityToken = await azdata.accounts.getAccountSecurityToken( + this.wizard.model.azureAccount, + this._subscriptionsMap.get(currentSubscriptionValue.name)?.tenant!, + azdata.AzureResource.ResourceManagement + ); + + this.populateResourceGroupDropdown(); + this.populateAzureRegionsDropdown(); + }); + } + + private async populateAzureSubscriptionsDropdown() { + this._azureSubscriptionsDropdown.loading = true; + let subService = await apiService.azurecoreApi; + let currentAccountDropdownValue = (this._azureAccountsDropdown.value as azdata.CategoryValue); + if (currentAccountDropdownValue === undefined) { + this._azureSubscriptionsDropdown.loading = false; + await this.populateResourceGroupDropdown(); + await this.populateAzureRegionsDropdown(); + return; + } + let currentAccount = this._accountsMap.get(currentAccountDropdownValue.name); + let subscriptions = (await subService.getSubscriptions(currentAccount, true)).subscriptions; + if (subscriptions === undefined || subscriptions.length === 0) { + this._azureSubscriptionsDropdown.updateProperties({ + values: [] + }); + this._azureSubscriptionsDropdown.loading = false; + await this.populateResourceGroupDropdown(); + await this.populateAzureRegionsDropdown(); + return; + } + subscriptions.sort((a: any, b: any) => a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase())); + + this.wizard.addDropdownValues( + this._azureSubscriptionsDropdown, + subscriptions.map((subscription: any): azdata.CategoryValue => { + let subscriptionCategoryValue = { + displayName: subscription.name + ' - ' + subscription.id, + name: subscription.id + }; + this._subscriptionsMap.set(subscriptionCategoryValue.name, subscription); + return subscriptionCategoryValue; + }) + ); + + this.wizard.model.azureSubscription = (this._azureSubscriptionsDropdown.value as azdata.CategoryValue).name; + this.wizard.model.azureSubscriptionDisplayName = (this._azureSubscriptionsDropdown.value as azdata.CategoryValue).displayName; + + this.wizard.model.securityToken = await azdata.accounts.getAccountSecurityToken( + this.wizard.model.azureAccount, + this._subscriptionsMap.get((this._azureSubscriptionsDropdown.value as azdata.CategoryValue).name)?.tenant!, + azdata.AzureResource.ResourceManagement + ); + this._azureSubscriptionsDropdown.loading = false; + await this.populateResourceGroupDropdown(); + await this.populateAzureRegionsDropdown(); + } + + private async createResourceDropdown(view: azdata.ModelView) { + this._resourceGroupDropdown = view.modelBuilder.dropDown().withProperties({ + required: true + }).component(); + this._resourceGroupDropdown.onValueChanged(async (value) => { + this.wizard.model.azureResouceGroup = value.selected; + }); + } + + private async populateResourceGroupDropdown() { + this._resourceGroupDropdown.loading = true; + let subService = await apiService.azurecoreApi; + let currentSubscriptionValue = this._azureSubscriptionsDropdown.value as azdata.CategoryValue; + if (currentSubscriptionValue === undefined || currentSubscriptionValue.displayName === '') { + + this._resourceGroupDropdown.updateProperties({ + values: [] + }); + this._resourceGroupDropdown.loading = false; + return; + } + let currentSubscription = this._subscriptionsMap.get(currentSubscriptionValue.name); + let resourceGroups = (await subService.getResourceGroups(this.wizard.model.azureAccount, currentSubscription, true)).resourceGroups; + if (resourceGroups === undefined || resourceGroups.length === 0) { + this._resourceGroupDropdown.loading = false; + this._resourceGroupDropdown.updateProperties({ + values: [] + }); + return; + } + + resourceGroups.sort((a: any, b: any) => a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase())); + this._resourceGroupDropdown.updateProperties({ + values: resourceGroups.map((resourceGroup: any) => { + return { + displayName: resourceGroup.name, + name: resourceGroup.name + }; + }) + }); + this.wizard.model.azureResouceGroup = (this._resourceGroupDropdown.value as azdata.CategoryValue).name; + this._resourceGroupDropdown.loading = false; + } + + private async createAzureRegionsDropdown(view: azdata.ModelView) { + this._azureRegionsDropdown = view.modelBuilder.dropDown().withProperties({ + required: true + }).component(); + + this._azureRegionsDropdown.onValueChanged((value) => { + this.wizard.model.azureRegion = (this._azureRegionsDropdown.value as azdata.CategoryValue).name; + }); + } + + private async populateAzureRegionsDropdown() { + this._azureRegionsDropdown.loading = true; + + let supportedRegions = 'eastus, eastus2, westus, centralus, northcentralus, southcentralus, northeurope, westeurope, eastasia, southeastasia, japaneast, japanwest, australiaeast, australiasoutheast, australiacentral, brazilsouth, southindia, centralindia, westindia, canadacentral, canadaeast, westus2, westcentralus, uksouth, ukwest, koreacentral, koreasouth, francecentral, southafricanorth, uaenorth, switzerlandnorth, germanywestcentral, norwayeast'; + let supportedRegionsArray = supportedRegions.split(', '); + let url = `https://management.azure.com/subscriptions/${this.wizard.model.azureSubscription}/locations?api-version=2020-01-01`; + const response = await this.wizard.getRequest(url, false); + response.data.value = response.data.value.sort((a: any, b: any) => (a.displayName > b.displayName) ? 1 : -1); + this.wizard.addDropdownValues( + this._azureRegionsDropdown, + response.data.value.filter((value: any) => { + return supportedRegionsArray.includes(value.name); + }).map((value: any) => { + return { + displayName: value.displayName, + name: value.name + }; + }) + ); + this.wizard.model.azureRegion = (this._azureRegionsDropdown.value as azdata.CategoryValue).name; + this._azureRegionsDropdown.loading = false; + } +} diff --git a/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/pages/basePage.ts b/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/pages/basePage.ts new file mode 100644 index 0000000000..05ea9fb22c --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/pages/basePage.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { WizardPageBase } from '../../wizardPageBase'; +import { DeployAzureSQLVMWizard } from '../deployAzureSQLVMWizard'; + +export abstract class BasePage extends WizardPageBase { + + protected liveValidation!: boolean; + + public initialize(): void { + throw new Error('Method not implemented.'); + } + + protected async validatePage(): Promise { + return ''; + } + + protected activateRealTimeFormValidation(): void { + if (this.liveValidation) { + this.validatePage(); + } + } +} diff --git a/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/pages/networkSettingsPage.ts b/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/pages/networkSettingsPage.ts new file mode 100644 index 0000000000..41025292f3 --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/pages/networkSettingsPage.ts @@ -0,0 +1,478 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import { DeployAzureSQLVMWizard } from '../deployAzureSQLVMWizard'; +import * as constants from '../constants'; +import { BasePage } from './basePage'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + + + +export class NetworkSettingsPage extends BasePage { + + // virtual network components + private _newVirtualNetworkCheckbox!: azdata.CheckBoxComponent; + private _virtualNetworkFlexContainer !: azdata.FlexContainer; + private _virtualNetworkDropdown!: azdata.DropDownComponent; + private _newVirtualNetworkText!: azdata.InputBoxComponent; + + // subnet network components + private _newSubnetCheckbox!: azdata.CheckBoxComponent; + private _subnetFlexContainer !: azdata.FlexContainer; + private _subnetDropdown!: azdata.DropDownComponent; + private _newsubnetText!: azdata.InputBoxComponent; + + // public ip components + private _newPublicIpCheckbox!: azdata.CheckBoxComponent; + private _publicIpFlexContainer !: azdata.FlexContainer; + private _publicIpDropdown!: azdata.DropDownComponent; + private _publicIpNetworkText!: azdata.InputBoxComponent; + + // checkbox for RDP + private _vmRDPAllowCheckbox!: azdata.CheckBoxComponent; + + private _form!: azdata.FormContainer; + + constructor(wizard: DeployAzureSQLVMWizard) { + super( + constants.NetworkSettingsPageTitle, + constants.NetworkSettingsPageDescription, + wizard + ); + } + + public async initialize() { + this.pageObject.registerContent(async (view: azdata.ModelView) => { + + await Promise.all([ + this.createVirtualNetworkDropdown(view), + this.createSubnetDropdown(view), + this.createPublicIPDropdown(view), + this.createVmRDPAllowCheckbox(view) + ]); + + + + this._form = view.modelBuilder.formContainer() + .withFormItems( + [ + { + component: this.wizard.createFormRowComponent(view, constants.VirtualNetworkDropdownLabel, '', this._virtualNetworkFlexContainer, true) + }, + { + component: this.wizard.createFormRowComponent(view, constants.SubnetDropdownLabel, '', this._subnetFlexContainer, true) + }, + { + component: this.wizard.createFormRowComponent(view, constants.PublicIPDropdownLabel, '', this._publicIpFlexContainer, true) + }, + { + component: this._vmRDPAllowCheckbox + } + ], + { + horizontal: false, + componentWidth: '100%' + }) + .withLayout({ width: '100%' }) + .component(); + + return view.initializeModel(this._form); + }); + } + + public async onEnter(): Promise { + this.populateVirtualNetworkDropdown(); + this.populatePublicIpkDropdown(); + this.liveValidation = false; + this.wizard.wizardObject.registerNavigationValidator(async (pcInfo) => { + if (pcInfo.newPage < pcInfo.lastPage) { + return true; + } + this.liveValidation = true; + let errorMessage = await this.validatePage(); + + if (errorMessage !== '') { + return false; + } + return true; + }); + } + + public async onLeave(): Promise { + this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { + return true; + }); + } + + private async createVirtualNetworkDropdown(view: azdata.ModelView) { + + this._newVirtualNetworkCheckbox = view.modelBuilder.checkBox().withProperties({ + label: constants.NetworkSettingsNewVirtualNetwork, + checked: false + }).component(); + + this._newVirtualNetworkCheckbox.onChanged((event) => { + this.toggleNewVirtualNetwork(); + }); + + this._virtualNetworkDropdown = view.modelBuilder.dropDown().withProperties({ + width: constants.standardWidth, + required: true + }).component(); + + this._virtualNetworkDropdown.onValueChanged((value) => { + this.wizard.model.virtualNetworkName = (this._virtualNetworkDropdown.value as azdata.CategoryValue).name; + this.populateSubnetDropdown(); + }); + + this._newVirtualNetworkText = view.modelBuilder.inputBox().withProperties({ + width: constants.standardWidth, + required: true, + placeHolder: localize('deployAzureSQLVM.NewVnetPlaceholder', "Enter name for new virtual network") + }).component(); + + this._newVirtualNetworkText.onTextChanged((e) => { + this.wizard.model.virtualNetworkName = e; + this.activateRealTimeFormValidation(); + }); + + this._virtualNetworkFlexContainer = view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column', + }).withItems( + [this._virtualNetworkDropdown, this._newVirtualNetworkText, this._newVirtualNetworkCheckbox] + ).component(); + + } + + private async populateVirtualNetworkDropdown() { + this._virtualNetworkDropdown.loading = true; + + let vnets = await this.getVirtualNetworks(); + if (!vnets || vnets.length === 0) { + vnets = [ + { + displayName: 'None', + name: 'None' + } + ]; + this._virtualNetworkDropdown.updateProperties({ + values: vnets + }); + this._newVirtualNetworkCheckbox.checked = true; + this._newVirtualNetworkCheckbox.enabled = false; + this.toggleNewVirtualNetwork(); + } else { + this._virtualNetworkDropdown.updateProperties({ + values: vnets + }); + this._newVirtualNetworkCheckbox.enabled = true; + this.toggleNewVirtualNetwork(); + } + this._virtualNetworkDropdown.loading = false; + + + await this.populateSubnetDropdown(); + } + + private toggleNewVirtualNetwork() { + + let newVirtualNetwork = this._newVirtualNetworkCheckbox.checked; + + this.wizard.model.newVirtualNetwork = newVirtualNetwork ? 'True' : 'False'; + + if (newVirtualNetwork) { + + this.wizard.changeComponentDisplay(this._virtualNetworkDropdown, 'none'); + this.wizard.changeComponentDisplay(this._newVirtualNetworkText, 'block'); + this._newSubnetCheckbox.enabled = false; + this.wizard.changeComponentDisplay(this._subnetDropdown, 'none'); + this.wizard.changeComponentDisplay(this._newsubnetText, 'block'); + this.wizard.model.virtualNetworkName = this._newVirtualNetworkText.value!; + this.wizard.model.newSubnet = 'True'; + this.wizard.model.subnetName = this._newsubnetText.value!; + + } else { + + this.wizard.changeComponentDisplay(this._virtualNetworkDropdown, 'block'); + this.wizard.changeComponentDisplay(this._newVirtualNetworkText, 'none'); + this._newSubnetCheckbox.enabled = true; + this.wizard.changeComponentDisplay(this._subnetDropdown, 'block'); + this.wizard.changeComponentDisplay(this._newsubnetText, 'none'); + this.wizard.model.virtualNetworkName = (this._virtualNetworkDropdown.value as azdata.CategoryValue).name; + this.wizard.model.newSubnet = this._newSubnetCheckbox.checked! ? 'True' : 'False'; + } + } + + private async createSubnetDropdown(view: azdata.ModelView) { + + this._newSubnetCheckbox = view.modelBuilder.checkBox().withProperties({ + label: constants.NetworkSettingsNewSubnet, + checked: false + }).component(); + + this._newSubnetCheckbox.onChanged((value) => { + this.toggleNewSubnet(); + }); + + + this._subnetDropdown = view.modelBuilder.dropDown().withProperties({ + width: constants.standardWidth, + required: true + }).component(); + + this._subnetDropdown.onValueChanged((value) => { + this.wizard.model.subnetName = (this._subnetDropdown.value as azdata.CategoryValue).name; + }); + + this._newsubnetText = view.modelBuilder.inputBox().withProperties({ + width: constants.standardWidth, + required: true, + placeHolder: localize('deployAzureSQLVM.NewSubnetPlaceholder', "Enter name for new subnet") + }).component(); + + this._newsubnetText.onTextChanged((e) => { + this.wizard.model.subnetName = e; + this.activateRealTimeFormValidation(); + }); + + this._subnetFlexContainer = view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column', + }).withItems( + [this._subnetDropdown, this._newsubnetText, this._newSubnetCheckbox] + ).component(); + + } + + + private async populateSubnetDropdown() { + this._subnetDropdown.loading = true; + + let subnets = await this.getSubnets(); + if (!subnets || subnets.length === 0) { + subnets = [{ + displayName: 'None', + name: 'None' + }]; + this._subnetDropdown.updateProperties({ + values: subnets + }); + this._newSubnetCheckbox.checked = true; + this._newSubnetCheckbox.enabled = false; + this.toggleNewSubnet(); + } else { + this._subnetDropdown.updateProperties({ + values: subnets + }); + this._newSubnetCheckbox.enabled = true; + this.toggleNewSubnet(); + } + + this._subnetDropdown.loading = false; + } + + private toggleNewSubnet() { + + let newSubnet = this._newSubnetCheckbox.checked!; + + this.wizard.model.newSubnet = newSubnet ? 'True' : 'False'; + + if (newSubnet) { + this.wizard.changeComponentDisplay(this._subnetDropdown, 'none'); + this.wizard.changeComponentDisplay(this._newsubnetText, 'block'); + this.wizard.model.subnetName = this._newsubnetText.value!; + } else { + this.wizard.changeComponentDisplay(this._subnetDropdown, 'block'); + this.wizard.changeComponentDisplay(this._newsubnetText, 'none'); + this.wizard.model.subnetName = (this._subnetDropdown.value as azdata.CategoryValue).name; + } + } + + private async createPublicIPDropdown(view: azdata.ModelView) { + + this._newPublicIpCheckbox = view.modelBuilder.checkBox().withProperties({ + label: constants.NetworkSettingsNewPublicIp, + checked: false + }).component(); + + this._newPublicIpCheckbox.onChanged((event) => { + this.toggleNewPublicIp(); + }); + + this._publicIpDropdown = view.modelBuilder.dropDown().withProperties({ + required: true, + width: constants.standardWidth, + }).component(); + + this._publicIpDropdown.onValueChanged((value) => { + this.wizard.model.publicIpName = (this._publicIpDropdown.value as azdata.CategoryValue).name; + }); + + this._publicIpNetworkText = view.modelBuilder.inputBox().withProperties({ + placeHolder: localize('deployAzureSQLVM.NewPipPlaceholder', "Enter name for new public IP"), + width: constants.standardWidth + }).component(); + + this._publicIpNetworkText.onTextChanged((e) => { + this.wizard.model.publicIpName = e; + this.activateRealTimeFormValidation(); + }); + + this.wizard.changeComponentDisplay(this._publicIpNetworkText, 'none'); + + this._publicIpFlexContainer = view.modelBuilder.flexContainer().withLayout({ + flexFlow: 'column', + }).withItems( + [this._publicIpDropdown, this._publicIpNetworkText, this._newPublicIpCheckbox] + ).component(); + + } + + private async populatePublicIpkDropdown() { + this._publicIpDropdown.loading = true; + + let publicIps = await this.getPips(); + + if (!publicIps || publicIps.length === 0) { + publicIps = [{ + displayName: 'None', + name: 'None' + }]; + this._publicIpDropdown.updateProperties({ + values: publicIps + }); + this._newPublicIpCheckbox.checked = true; + this._newPublicIpCheckbox.enabled = false; + + this.toggleNewPublicIp(); + } else { + this._publicIpDropdown.updateProperties({ + values: publicIps + }); + this._newPublicIpCheckbox.enabled = true; + + this.toggleNewPublicIp(); + } + this._publicIpDropdown.loading = false; + } + + private toggleNewPublicIp() { + let newPip = this._newPublicIpCheckbox.checked!; + + this.wizard.model.newPublicIp = newPip ? 'True' : 'False'; + + if (newPip) { + this.wizard.changeComponentDisplay(this._publicIpDropdown, 'none'); + this.wizard.changeComponentDisplay(this._publicIpNetworkText, 'block'); + this.wizard.model.publicIpName = this._publicIpNetworkText.value!; + } else { + this.wizard.changeComponentDisplay(this._publicIpDropdown, 'block'); + this.wizard.changeComponentDisplay(this._publicIpNetworkText, 'none'); + this.wizard.model.publicIpName = (this._publicIpDropdown.value as azdata.CategoryValue).name; + } + } + + private async createVmRDPAllowCheckbox(view: azdata.ModelView) { + this._vmRDPAllowCheckbox = view.modelBuilder.checkBox().withProperties({ + label: constants.RDPAllowCheckboxLabel, + }).component(); + this._vmRDPAllowCheckbox.onChanged((value) => { + this.wizard.model.allowRDP = (value) ? 'True' : 'False'; + }); + this.wizard.model.allowRDP = 'False'; + } + + + public async getVirtualNetworks(): Promise { + let url = `https://management.azure.com` + + `/subscriptions/${this.wizard.model.azureSubscription}` + + `/providers/Microsoft.Network/virtualNetworks?api-version=2020-05-01`; + + let response = await this.wizard.getRequest(url); + + let dropdownValues = response.data.value.filter((value: any) => { + return value.location === this.wizard.model.azureRegion; + }).map((value: any) => { + let resourceGroupName = value.id.replace(RegExp('^(.*?)/resourceGroups/'), '').replace(RegExp('/providers/.*'), ''); + return { + name: value.id, + displayName: `${value.name} \t\t resource group: (${resourceGroupName})` + }; + }); + return dropdownValues; + } + + public async getSubnets(): Promise { + if (!this.wizard.model.virtualNetworkName) { + return; + } + let url = `https://management.azure.com` + + `${this.wizard.model.virtualNetworkName}` + + `/subnets?api-version=2020-05-01`; + let response = await this.wizard.getRequest(url); + let dropdownValues = response.data.value.map((value: any) => { + return { + name: value.id, + displayName: `${value.name}` + }; + }); + return dropdownValues; + } + + public async getPips(): Promise { + let url = `https://management.azure.com` + + `/subscriptions/${this.wizard.model.azureSubscription}` + + `/providers/Microsoft.Network/publicIPAddresses?api-version=2020-05-01`; + let response = await this.wizard.getRequest(url); + let dropdownValues = response.data.value.filter((value: any) => { + return value.location === this.wizard.model.azureRegion; + }).map((value: any) => { + let resourceGroupName = value.id.replace(RegExp('^(.*?)/resourceGroups/'), '').replace(RegExp('/providers/.*'), ''); + return { + name: value.id, + displayName: `${value.name} \t\t resource group: (${resourceGroupName})` + }; + }); + return dropdownValues; + } + + protected async validatePage(): Promise { + const errorMessages = []; + if (this.wizard.model.newVirtualNetwork === 'True') { + if (this.wizard.model.virtualNetworkName.length < 2 || this.wizard.model.virtualNetworkName.length > 64) { + errorMessages.push(localize('deployAzureSQLVM.VnetNameLengthError', "Virtual Network name must be between 2 and 64 characters long")); + } + } else { + if (this.wizard.model.virtualNetworkName === 'None') { + errorMessages.push(localize('deployAzureSQLVM.NewVnetError', "Create a new virtual network")); + } + } + + if (this.wizard.model.newSubnet === 'True') { + if (this.wizard.model.subnetName.length < 1 || this.wizard.model.subnetName.length > 80) { + errorMessages.push(localize('deployAzureSQLVM.SubnetNameLengthError', "Subnet name must be between 1 and 80 characters long")); + } + } else { + if (this.wizard.model.subnetName === 'None') { + errorMessages.push(localize('deployAzureSQLVM.NewSubnetError', "Create a new sub network")); + } + } + + if (this.wizard.model.newPublicIp === 'True') { + if (this.wizard.model.publicIpName.length < 1 || this.wizard.model.publicIpName.length > 80) { + errorMessages.push(localize('deployAzureSQLVM.PipNameError', "Public IP name must be between 1 and 80 characters long")); + } + } else { + if (this.wizard.model.publicIpName === 'None') { + errorMessages.push(localize('deployAzureSQLVM.NewPipError', "Create a new new public Ip")); + } + } + + this.wizard.showErrorMessage(errorMessages.join('\n')); + return errorMessages.join('\n'); + + } +} diff --git a/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/pages/sqlServerSettingsPage.ts b/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/pages/sqlServerSettingsPage.ts new file mode 100644 index 0000000000..56691f4c40 --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/pages/sqlServerSettingsPage.ts @@ -0,0 +1,249 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import { EOL } from 'os'; +import * as constants from '../constants'; +import { DeployAzureSQLVMWizard } from '../deployAzureSQLVMWizard'; +import { BasePage } from './basePage'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +export class SqlServerSettingsPage extends BasePage { + + private _sqlConnectivityDropdown!: azdata.DropDownComponent; + private _portTextRow!: azdata.FlexContainer; + private _portTextBox!: azdata.InputBoxComponent; + private _sqlAuthenticationDropdown!: azdata.DropDownComponent; + private _sqlAuthenticationTextbox!: azdata.InputBoxComponent; + private _sqlAuthenticationTextRow!: azdata.FlexContainer; + private _sqlAuthenticationPasswordTextbox!: azdata.InputBoxComponent; + private _sqlAuthenticationPasswordTextRow!: azdata.FlexContainer; + private _sqlAuthenticationPasswordConfirmationTextbox!: azdata.InputBoxComponent; + private _sqlAuthenticationPasswordConfirmationTextRow!: azdata.FlexContainer; + //private _sqlStorageOptimiazationDropdown!: azdata.DropDownComponent; + //private sqlStorageContainer!: azdata.FlexContainer; + + private _form!: azdata.FormContainer; + + constructor(wizard: DeployAzureSQLVMWizard) { + super( + constants.SqlServerSettingsPageTitle, + '', + wizard + ); + + } + + public async initialize() { + this.pageObject.registerContent(async (view: azdata.ModelView) => { + + await Promise.all([ + this.createSqlConnectivityDropdown(view), + this.createPortText(view), + this.createSqlAuthentication(view), + ]); + + this._form = view.modelBuilder.formContainer() + .withFormItems( + [ + { + component: this.wizard.createFormRowComponent(view, constants.SqlConnectivityTypeDropdownLabel, '', this._sqlConnectivityDropdown, true) + }, + { + component: this._portTextRow + }, + { + component: this.wizard.createFormRowComponent(view, constants.SqlEnableSQLAuthenticationLabel, '', this._sqlAuthenticationDropdown, true) + }, + { + component: this._sqlAuthenticationTextRow + }, + { + component: this._sqlAuthenticationPasswordTextRow + }, + { + component: this._sqlAuthenticationPasswordConfirmationTextRow + } + ], + { + horizontal: false, + componentWidth: '100%' + }) + .withLayout({ width: '100%' }) + .component(); + + return view.initializeModel(this._form); + }); + } + + public async onEnter(): Promise { + + this.liveValidation = false; + + this.wizard.wizardObject.registerNavigationValidator(async (pcInfo) => { + if (pcInfo.newPage < pcInfo.lastPage) { + return true; + } + + this.liveValidation = true; + + let showErrorMessage = await this.validatePage(); + + if (showErrorMessage !== '') { + return false; + } + return true; + }); + } + + public async onLeave(): Promise { + this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { + return true; + }); + } + + private createSqlConnectivityDropdown(view: azdata.ModelView) { + + const privateOptionDisplayName = localize('deployAzureSQLVM.PrivateConnectivityDropdownOptionDefault', "Private (within Virtual Network)"); + this._sqlConnectivityDropdown = view.modelBuilder.dropDown().withProperties( + { + values: [ + { + name: 'local', + displayName: localize('deployAzureSQLVM.LocalConnectivityDropdownOption', "Local (inside VM only)") + }, + { + name: 'private', + displayName: privateOptionDisplayName + }, + { + name: 'public', + displayName: localize('deployAzureSQLVM.PublicConnectivityDropdownOption', "Public (Internet)") + } + ], + value: { + name: 'private', + displayName: privateOptionDisplayName + } + }).component(); + + this.wizard.model.sqlConnectivityType = (this._sqlConnectivityDropdown.value as azdata.CategoryValue).name; + + this._sqlConnectivityDropdown.onValueChanged((value) => { + + let connectivityValue = (this._sqlConnectivityDropdown.value as azdata.CategoryValue).name; + this.wizard.model.sqlConnectivityType = connectivityValue; + + if (connectivityValue === 'local') { + this.wizard.changeRowDisplay(this._portTextRow, 'none'); + } else { + this.wizard.changeRowDisplay(this._portTextRow, 'block'); + } + }); + + } + + private createPortText(view: azdata.ModelView) { + this._portTextBox = view.modelBuilder.inputBox().withProperties({ + inputType: 'number', + max: 65535, + min: 1024, + value: '1433' + }).component(); + + this._portTextBox.onTextChanged((value) => { + this.wizard.model.port = value; + this.activateRealTimeFormValidation(); + }); + + this._portTextRow = this.wizard.createFormRowComponent(view, constants.SqlPortLabel, '', this._portTextBox, true); + } + + private createSqlAuthentication(view: azdata.ModelView) { + + this._sqlAuthenticationDropdown = view.modelBuilder.dropDown().withProperties({ + values: [ + { + displayName: localize('deployAzureSQLVM.EnableSqlAuthenticationYesOption', "Yes"), + name: 'True' + }, + { + displayName: localize('deployAzureSQLVM.EnableSqlAuthenticationNoOption', "No"), + name: 'False' + } + ] + }).component(); + + this._sqlAuthenticationDropdown.onValueChanged((value) => { + let dropdownValue = (this._sqlAuthenticationDropdown.value as azdata.CategoryValue).name; + let displayValue: 'block' | 'none' = dropdownValue === 'True' ? 'block' : 'none'; + this.wizard.changeRowDisplay(this._sqlAuthenticationTextRow, displayValue); + this.wizard.changeRowDisplay(this._sqlAuthenticationPasswordTextRow, displayValue); + this.wizard.changeRowDisplay(this._sqlAuthenticationPasswordConfirmationTextRow, displayValue); + this.wizard.model.enableSqlAuthentication = dropdownValue; + }); + + this.wizard.model.enableSqlAuthentication = (this._sqlAuthenticationDropdown.value as azdata.CategoryValue).name; + + + this._sqlAuthenticationTextbox = view.modelBuilder.inputBox().component(); + + this._sqlAuthenticationTextRow = this.wizard.createFormRowComponent(view, constants.SqlAuthenticationUsernameLabel, '', this._sqlAuthenticationTextbox, true); + + this._sqlAuthenticationPasswordTextbox = view.modelBuilder.inputBox().withProperties({ + inputType: 'password' + }).component(); + + this._sqlAuthenticationPasswordTextRow = this.wizard.createFormRowComponent(view, constants.SqlAuthenticationPasswordLabel, '', this._sqlAuthenticationPasswordTextbox, true); + + this._sqlAuthenticationPasswordConfirmationTextbox = view.modelBuilder.inputBox().withProperties({ + inputType: 'password' + }).component(); + + this._sqlAuthenticationPasswordConfirmationTextRow = this.wizard.createFormRowComponent(view, constants.SqlAuthenticationConfirmPasswordLabel, '', this._sqlAuthenticationPasswordConfirmationTextbox, true); + + + this._sqlAuthenticationTextbox.onTextChanged((value) => { + this.wizard.model.sqlAuthenticationUsername = value; + this.activateRealTimeFormValidation(); + }); + + this._sqlAuthenticationPasswordTextbox.onTextChanged((value) => { + this.wizard.model.sqlAuthenticationPassword = value; + this.activateRealTimeFormValidation(); + }); + + } + + + protected async validatePage(): Promise { + + const errorMessages = []; + + if ((this._sqlAuthenticationDropdown.value as azdata.CategoryValue).name === 'True') { + let username = this._sqlAuthenticationTextbox.value!; + + if (username.length < 2 || username.length > 128) { + errorMessages.push(localize('deployAzureSQLVM.SqlUsernameLengthError', "Username must be between 2 and 128 characters long.")); + } + + if (/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]+/.test(username)) { + errorMessages.push(localize('deployAzureSQLVM.SqlUsernameSpecialCharError', "Username cannot contain special characters \/\"\"[]:|<>+=;,?* .")); + } + + errorMessages.push(this.wizard.validatePassword(this._sqlAuthenticationPasswordTextbox.value!)); + + if (this._sqlAuthenticationPasswordTextbox.value !== this._sqlAuthenticationPasswordConfirmationTextbox.value) { + errorMessages.push(localize('deployAzureSQLVM.SqlConfirmPasswordError', "Password and confirm password must match.")); + } + } + + + this.wizard.showErrorMessage(errorMessages.join(EOL)); + + return errorMessages.join(EOL); + } +} diff --git a/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/pages/summaryPage.ts b/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/pages/summaryPage.ts new file mode 100644 index 0000000000..8a71549101 --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/pages/summaryPage.ts @@ -0,0 +1,367 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import { WizardPageBase } from '../../wizardPageBase'; +import { DeployAzureSQLVMWizard } from '../deployAzureSQLVMWizard'; +import * as constants from '../constants'; +import { SectionInfo, LabelPosition, FontWeight, FieldType } from '../../../interfaces'; +import { createSection } from '../../modelViewUtils'; + +export class AzureSQLVMSummaryPage extends WizardPageBase { + + private formItems: azdata.FormComponent[] = []; + private _form!: azdata.FormBuilder; + private _view!: azdata.ModelView; + + constructor(wizard: DeployAzureSQLVMWizard) { + super( + 'Summary', + '', + wizard + ); + + } + + public async initialize() { + this.pageObject.registerContent(async (view: azdata.ModelView) => { + this._view = view; + this._form = view.modelBuilder.formContainer(); + return view.initializeModel(this._form!.withLayout({ width: '100%' }).component()); + }); + } + + public async onEnter(): Promise { + + this.formItems.forEach(item => { + this._form.removeFormItem(item); + }); + + this.formItems = []; + + let model = this.wizard.model; + + const labelWidth = '150px'; + const inputWidth = '400px'; + const fieldHeight = '20px'; + + const auzreSettingSection: SectionInfo = { + labelPosition: LabelPosition.Left, + labelWidth: labelWidth, + inputWidth: inputWidth, + fieldHeight: fieldHeight, + spaceBetweenFields: '0', + title: constants.AzureSettingsPageTitle, + rows: [ + { + items: [ + { + type: FieldType.ReadonlyText, + label: constants.AzureAccountDropdownLabel, + defaultValue: model.azureAccount.displayInfo.displayName, + labelCSSStyles: { fontWeight: FontWeight.Bold } + }, + ] + }, + { + items: [ + { + type: FieldType.ReadonlyText, + label: constants.AzureAccountSubscriptionDropdownLabel, + defaultValue: model.azureSubscriptionDisplayName, + labelCSSStyles: { fontWeight: FontWeight.Bold } + } + ] + }, + { + items: [ + { + type: FieldType.ReadonlyText, + label: constants.AzureAccountResourceGroupDropdownLabel, + defaultValue: model.azureResouceGroup, + labelCSSStyles: { fontWeight: FontWeight.Bold } + } + ] + }, + { + items: [ + { + type: FieldType.ReadonlyText, + label: constants.AzureAccountRegionDropdownLabel, + defaultValue: model.azureRegion, + labelCSSStyles: { fontWeight: FontWeight.Bold } + } + ] + } + ] + }; + + const vmSettingSection: SectionInfo = { + labelPosition: LabelPosition.Left, + labelWidth: labelWidth, + inputWidth: inputWidth, + fieldHeight: fieldHeight, + title: constants.VmSettingsPageTitle, + rows: [ + { + items: [ + { + type: FieldType.ReadonlyText, + label: constants.VmNameTextBoxLabel, + defaultValue: model.vmName, + labelCSSStyles: { fontWeight: FontWeight.Bold } + } + ] + }, + { + items: [ + { + type: FieldType.ReadonlyText, + label: constants.SqlAuthenticationUsernameLabel, + defaultValue: model.vmUsername, + labelCSSStyles: { fontWeight: FontWeight.Bold } + }, + ] + }, + { + items: [ + { + type: FieldType.ReadonlyText, + label: constants.VmImageDropdownLabel, + defaultValue: model.vmImage, + labelCSSStyles: { fontWeight: FontWeight.Bold } + } + ] + }, + { + items: [ + { + type: FieldType.ReadonlyText, + label: constants.VmSkuDropdownLabel, + defaultValue: model.vmImageSKU, + labelCSSStyles: { fontWeight: FontWeight.Bold } + }, + ] + }, + { + items: [ + { + type: FieldType.ReadonlyText, + label: constants.VmVersionDropdownLabel, + defaultValue: model.vmImageVersion, + labelCSSStyles: { fontWeight: FontWeight.Bold } + } + ] + }, + { + items: [ + { + type: FieldType.ReadonlyText, + label: constants.VmSizeDropdownLabel, + defaultValue: model.vmSize, + labelCSSStyles: { fontWeight: FontWeight.Bold } + } + ] + }, + ] + }; + + const networkSettingSection: SectionInfo = { + labelPosition: LabelPosition.Left, + labelWidth: labelWidth, + inputWidth: inputWidth, + fieldHeight: fieldHeight, + title: constants.NetworkSettingsPageTitle, + rows: [ + { + items: [ + { + type: FieldType.ReadonlyText, + label: constants.VirtualNetworkDropdownLabel, + defaultValue: ((model.newVirtualNetwork === 'True' ? '(new) ' : '') + this.processVnetName()), + labelCSSStyles: { fontWeight: FontWeight.Bold } + }, + ] + + }, + { + items: [ + { + type: FieldType.ReadonlyText, + label: constants.SubnetDropdownLabel, + defaultValue: ((model.newSubnet === 'True' ? '(new) ' : '') + this.processSubnetName()), + labelCSSStyles: { fontWeight: FontWeight.Bold } + }, + ] + + }, + { + items: [ + { + type: FieldType.ReadonlyText, + label: constants.PublicIPDropdownLabel, + defaultValue: ((model.newPublicIp === 'True' ? '(new) ' : '') + this.processPublicIp()), + labelCSSStyles: { fontWeight: FontWeight.Bold } + } + ] + } + ] + }; + + const sqlServerSettingsPage: SectionInfo = { + labelPosition: LabelPosition.Left, + labelWidth: labelWidth, + inputWidth: inputWidth, + fieldHeight: fieldHeight, + title: constants.SqlServerSettingsPageTitle, + rows: [ + ] + }; + + sqlServerSettingsPage.rows?.push({ + items: [ + { + type: FieldType.ReadonlyText, + label: constants.SqlConnectivityTypeDropdownLabel, + defaultValue: model.sqlConnectivityType, + labelCSSStyles: { fontWeight: FontWeight.Bold } + } + ] + }); + + if (model.sqlConnectivityType !== 'local') { + sqlServerSettingsPage.rows?.push({ + items: [ + { + type: FieldType.ReadonlyText, + label: constants.SqlPortLabel, + defaultValue: constants.SqlPortLabel, + labelCSSStyles: { fontWeight: FontWeight.Bold } + } + ] + }); + } + + + sqlServerSettingsPage.rows?.push({ + items: [ + { + type: FieldType.ReadonlyText, + label: constants.SqlEnableSQLAuthenticationLabel, + defaultValue: (model.enableSqlAuthentication === 'True' ? 'Yes ' : 'No '), + labelCSSStyles: { fontWeight: FontWeight.Bold } + } + ] + }); + + if (model.enableSqlAuthentication === 'True') { + sqlServerSettingsPage.rows?.push({ + items: [ + { + type: FieldType.ReadonlyText, + label: constants.SqlAuthenticationUsernameLabel, + defaultValue: model.sqlAuthenticationUsername, + labelCSSStyles: { fontWeight: FontWeight.Bold } + } + ] + }); + } + + const createSectionFunc = async (sectionInfo: SectionInfo): Promise => { + return { + title: '', + component: await createSection({ + container: this.wizard.wizardObject, + inputComponents: {}, + sectionInfo: sectionInfo, + view: this._view, + onNewDisposableCreated: () => { }, + onNewInputComponentCreated: () => { }, + onNewValidatorCreated: () => { }, + toolsService: this.wizard.toolsService + }) + }; + }; + + const azureSection = await createSectionFunc(auzreSettingSection); + const vmSection = await createSectionFunc(vmSettingSection); + const networkSection = await createSectionFunc(networkSettingSection); + const sqlServerSection = await createSectionFunc(sqlServerSettingsPage); + + + this.formItems.push(azureSection, vmSection, networkSection, sqlServerSection); + this._form.addFormItems(this.formItems); + + this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { + return true; + }); + } + + public async onLeave(): Promise { + this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { + return true; + }); + } + + public createSummaryRow(view: azdata.ModelView, title: string, textComponent: azdata.TextComponent): azdata.FlexContainer { + + const labelText = view.modelBuilder.text() + .withProperties( + { + value: title, + width: '250px', + }) + .component(); + + labelText.updateCssStyles({ + 'font-weight': '400', + 'font-size': '13px', + }); + + const flexContainer = view.modelBuilder.flexContainer() + .withLayout( + { + flexFlow: 'row', + alignItems: 'center', + }) + .withItems( + [labelText, textComponent], + { + CSSStyles: { 'margin-right': '5px' } + }) + .component(); + + return flexContainer; + } + + public processVnetName(): string { + if (this.wizard.model.newVirtualNetwork === 'True') { + return this.wizard.model.virtualNetworkName; + } + + let resourceGroupName = this.wizard.model.virtualNetworkName.replace(RegExp('^(.*?)/resourceGroups/'), '').replace(RegExp('/providers/.*'), ''); + let vnetName = this.wizard.model.virtualNetworkName.replace(RegExp('^(.*?)/virtualNetworks/'), ''); + return `(${resourceGroupName}) ${vnetName}`; + } + + public processSubnetName(): string { + if (this.wizard.model.newSubnet === 'True') { + return this.wizard.model.subnetName; + } + + let subnetName = this.wizard.model.subnetName.replace(RegExp('^(.*?)/subnets/'), ''); + return `${subnetName}`; + } + + public processPublicIp(): string { + if (this.wizard.model.newPublicIp === 'True') { + return this.wizard.model.publicIpName; + } + + let resourceGroupName = this.wizard.model.publicIpName.replace(RegExp('^(.*?)/resourceGroups/'), '').replace(RegExp('/providers/.*'), ''); + let pipName = this.wizard.model.publicIpName.replace(RegExp('^(.*?)/publicIPAddresses/'), ''); + return `(${resourceGroupName}) ${pipName}`; + } +} diff --git a/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/pages/vmSettingsPage.ts b/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/pages/vmSettingsPage.ts new file mode 100644 index 0000000000..a1e0339a23 --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/pages/vmSettingsPage.ts @@ -0,0 +1,467 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import { EOL } from 'os'; +import * as constants from '../constants'; +import { DeployAzureSQLVMWizard } from '../deployAzureSQLVMWizard'; +import { BasePage } from './basePage'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +export class VmSettingsPage extends BasePage { + + private _vmSize: string[] = []; + + // textbox for vm name + private _vmNameTextBox!: azdata.InputBoxComponent; + + // textbox for vm admin username + private _adminUsernameTextBox!: azdata.InputBoxComponent; + + // textbox for vm admin password + private _adminPasswordTextBox!: azdata.InputBoxComponent; + + // textbox for vm admin confirm password + private _adminComfirmPasswordTextBox!: azdata.InputBoxComponent; + + // dropdown for sql vm image + private _vmImageDropdown!: azdata.DropDownComponent; + + // dropdown for sql vm image sku <- sql vm image + private _vmImageSkuDropdown!: azdata.DropDownComponent; + + // dropdown for sql vm image version <- sql vm image sku + private _vmImageVersionDropdown!: azdata.DropDownComponent; + + // dropdown for sql vm size + private _vmSizeDropdown!: azdata.DropDownComponent; + private _vmSizeLearnMoreLink!: azdata.HyperlinkComponent; + + private _form!: azdata.FormContainer; + + constructor(wizard: DeployAzureSQLVMWizard) { + super( + constants.VmSettingsPageTitle, + '', + wizard + ); + } + + public async initialize() { + this.pageObject.registerContent(async (view: azdata.ModelView) => { + + await Promise.all([ + this.createVmNameTextBox(view), + this.createAdminUsernameTextBox(view), + this.createAdminPasswordTextBox(view), + this.createAdminPasswordConfirmTextBox(view), + this.createVmImageDropdown(view), + this.createVMImageSkuDropdown(view), + this.createVMImageVersionDropdown(view), + this.createVmSizeDropdown(view), + ]); + + + this.liveValidation = false; + + this._form = view.modelBuilder.formContainer() + .withFormItems( + [ + { + component: this.wizard.createFormRowComponent(view, constants.VmNameTextBoxLabel, '', this._vmNameTextBox, true) + }, + { + component: this.wizard.createFormRowComponent(view, constants.VmAdminUsernameTextBoxLabel, '', this._adminUsernameTextBox, true) + }, + { + component: this.wizard.createFormRowComponent(view, constants.VmAdminPasswordTextBoxLabel, '', this._adminPasswordTextBox, true) + }, + { + component: this.wizard.createFormRowComponent(view, constants.VmAdminConfirmPasswordTextBoxLabel, '', this._adminComfirmPasswordTextBox, true) + }, + { + component: this.wizard.createFormRowComponent(view, constants.VmImageDropdownLabel, '', this._vmImageDropdown, true) + }, + { + component: this.wizard.createFormRowComponent(view, constants.VmSkuDropdownLabel, '', this._vmImageSkuDropdown, true) + }, + { + component: this.wizard.createFormRowComponent(view, constants.VmVersionDropdownLabel, '', this._vmImageVersionDropdown, true) + }, + { + component: this.wizard.createFormRowComponent(view, constants.VmSizeDropdownLabel, '', this._vmSizeDropdown, true) + }, + { + component: this._vmSizeLearnMoreLink + } + ], + { + horizontal: false, + componentWidth: '100%' + }) + .withLayout({ width: '100%' }) + .component(); + + + return view.initializeModel(this._form); + }); + } + + public async onEnter(): Promise { + this.populateVmImageDropdown(); + this.populateVmSizeDropdown(); + + this.liveValidation = false; + + this.wizard.wizardObject.registerNavigationValidator(async (pcInfo) => { + this.liveValidation = true; + + if (pcInfo.newPage < pcInfo.lastPage) { + return true; + } + + let errorMessage = await this.validatePage(); + + if (errorMessage !== '') { + return false; + } + return true; + }); + } + + public async onLeave(): Promise { + this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { + return true; + }); + } + + + private async createVmNameTextBox(view: azdata.ModelView) { + this._vmNameTextBox = view.modelBuilder.inputBox().withProperties({ + }).component(); + + this._vmNameTextBox.onTextChanged((value) => { + this.wizard.model.vmName = value; + this.activateRealTimeFormValidation(); + }); + } + + private async createAdminUsernameTextBox(view: azdata.ModelView) { + this._adminUsernameTextBox = view.modelBuilder.inputBox().withProperties({ + }).component(); + + this._adminUsernameTextBox.onTextChanged((value) => { + this.wizard.model.vmUsername = value; + this.activateRealTimeFormValidation(); + }); + } + + private async createAdminPasswordTextBox(view: azdata.ModelView) { + this._adminPasswordTextBox = view.modelBuilder.inputBox().withProperties({ + inputType: 'password', + }).component(); + + this._adminPasswordTextBox.onTextChanged((value) => { + this.wizard.model.vmPassword = value; + this.activateRealTimeFormValidation(); + }); + } + + private async createAdminPasswordConfirmTextBox(view: azdata.ModelView) { + this._adminComfirmPasswordTextBox = view.modelBuilder.inputBox().withProperties({ + inputType: 'password', + }).component(); + + this._adminComfirmPasswordTextBox.onTextChanged((value) => { + this.activateRealTimeFormValidation(); + }); + } + + private async createVmImageDropdown(view: azdata.ModelView) { + this._vmImageDropdown = view.modelBuilder.dropDown().withProperties({ + }).component(); + + this._vmImageDropdown.onValueChanged((value) => { + this.wizard.model.vmImage = (this._vmImageDropdown.value as azdata.CategoryValue).name; + this._vmImageSkuDropdown.loading = true; + this._vmImageVersionDropdown.loading = true; + this.populateVmImageSkuDropdown(); + }); + + } + + private async populateVmImageDropdown() { + this._vmImageDropdown.loading = true; + this._vmImageSkuDropdown.loading = true; + this._vmImageVersionDropdown.loading = true; + + let url = `https://management.azure.com` + + `/subscriptions/${this.wizard.model.azureSubscription}` + + `/providers/Microsoft.Compute` + + `/locations/${this.wizard.model.azureRegion}` + + `/publishers/MicrosoftSQLServer` + + `/artifacttypes/vmimage/offers` + + `?api-version=2019-12-01`; + + let response = await this.wizard.getRequest(url, true); + response.data = response.data.reverse(); + this.wizard.addDropdownValues( + this._vmImageDropdown, + response.data.filter((value: any) => { + return !new RegExp('-byol').test(value.name.toLowerCase()); + }) + .map((value: any) => { + let sqlServerVersion = value.name.toLowerCase().match(new RegExp('sql(.*?)-'))[1]; + let osVersion = value.name.toLowerCase().replace(new RegExp('sql(.*?)-'), ''); + osVersion = osVersion.replace(new RegExp('ws'), 'Windows Server '); + osVersion = osVersion.replace(new RegExp('ubuntu'), 'Ubuntu Server '); + osVersion = osVersion.replace(new RegExp('sles'), 'SUSE Linux Enterprise Server (SLES) '); + osVersion = osVersion.replace(new RegExp('rhel'), 'Red Hat Enterprise Linux '); + return { + displayName: `SQL Server ${sqlServerVersion.toUpperCase()} on ${osVersion}`, + name: value.name + }; + }) + ); + + this.wizard.model.vmImage = (this._vmImageDropdown.value as azdata.CategoryValue).name; + this._vmImageDropdown.loading = false; + this.populateVmImageSkuDropdown(); + } + + private async createVMImageSkuDropdown(view: azdata.ModelView) { + this._vmImageSkuDropdown = view.modelBuilder.dropDown().withProperties({ + }).component(); + + this._vmImageSkuDropdown.onValueChanged((value) => { + this.wizard.model.vmImageSKU = (this._vmImageSkuDropdown.value as azdata.CategoryValue).name; + this.populateVmImageVersionDropdown(); + }); + + } + + private async populateVmImageSkuDropdown() { + this._vmImageSkuDropdown.loading = true; + let url = `https://management.azure.com` + + `/subscriptions/${this.wizard.model.azureSubscription}` + + `/providers/Microsoft.Compute` + + `/locations/${this.wizard.model.azureRegion}` + + `/publishers/MicrosoftSQLServer` + + `/artifacttypes/vmimage/offers/${this.wizard.model.vmImage}` + + `/skus?api-version=2019-12-01`; + + let response = await this.wizard.getRequest(url, true); + + this.wizard.addDropdownValues( + this._vmImageSkuDropdown, + response.data.map((value: any) => { + return { + name: value.name, + displayName: value.name + }; + }) + ); + + this.wizard.model.vmImageSKU = (this._vmImageSkuDropdown.value as azdata.CategoryValue).name; + this._vmImageSkuDropdown.loading = false; + this.populateVmImageVersionDropdown(); + } + + private async createVMImageVersionDropdown(view: azdata.ModelView) { + this._vmImageVersionDropdown = view.modelBuilder.dropDown().withProperties({ + }).component(); + + this._vmImageVersionDropdown.onValueChanged((value) => { + this.wizard.model.vmImageVersion = (this._vmImageVersionDropdown.value as azdata.CategoryValue).name; + }); + } + + private async populateVmImageVersionDropdown() { + this._vmImageVersionDropdown.loading = true; + let url = `https://management.azure.com` + + `/subscriptions/${this.wizard.model.azureSubscription}` + + `/providers/Microsoft.Compute` + + `/locations/${this.wizard.model.azureRegion}` + + `/publishers/MicrosoftSQLServer` + + `/artifacttypes/vmimage/offers/${this.wizard.model.vmImage}` + + `/skus/${this.wizard.model.vmImageSKU}` + + `/versions?api-version=2019-12-01`; + + let response = await this.wizard.getRequest(url, true); + + this.wizard.addDropdownValues( + this._vmImageVersionDropdown, + response.data.map((value: any) => { + return { + name: value.name, + displayName: value.name + }; + }) + ); + + this.wizard.model.vmImageVersion = (this._vmImageVersionDropdown.value as azdata.CategoryValue).name; + this._vmImageVersionDropdown.loading = false; + } + + + private async createVmSizeDropdown(view: azdata.ModelView) { + this._vmSizeDropdown = view.modelBuilder.dropDown().withProperties({ + editable: true + }).component(); + + this._vmSizeDropdown.onValueChanged((value) => { + this.wizard.model.vmSize = (this._vmSizeDropdown.value as azdata.CategoryValue).name; + }); + + this._vmSizeLearnMoreLink = view.modelBuilder.hyperlink().withProperties({ + label: constants.VmSizeLearnMoreLabel, + url: 'https://go.microsoft.com/fwlink/?linkid=2143101' + + }).component(); + } + + private async populateVmSizeDropdown() { + this._vmSizeDropdown.loading = true; + let url = `https://management.azure.com` + + `/subscriptions/${this.wizard.model.azureSubscription}` + + `/providers/Microsoft.Compute` + + `/skus?api-version=2019-04-01` + + `&$filter=location eq '${this.wizard.model.azureRegion}'`; + + let response = await this.wizard.getRequest(url, true); + + let vmResouces: any[] = []; + response.data.value.map((res: any) => { + if (res.resourceType === 'virtualMachines') { + vmResouces.push(res); + } + }); + + let dropDownValues = vmResouces.filter((value: any) => { + const discSize = Number(value.capabilities.filter((c: any) => { return c.name === 'MaxResourceVolumeMB'; })[0].value) / 1024; + if (discSize >= 40) { + return value; + } + }).map((value: any) => { + if (value.capabilities) { + let cores; + if (value.capabilities.filter((c: any) => { return c.name === 'vCPUsAvailable'; }).length !== 0) { + cores = value.capabilities.filter((c: any) => { return c.name === 'vCPUsAvailable'; })[0].value; + } else { + cores = value.capabilities.filter((c: any) => { return c.name === 'vCPUs'; })[0].value; + } + const memory = value.capabilities.filter((c: any) => { return c.name === 'MemoryGB'; })[0].value; + const discSize = Number(value.capabilities.filter((c: any) => { return c.name === 'MaxResourceVolumeMB'; })[0].value) / 1024; + const discCount = value.capabilities.filter((c: any) => { return c.name === 'MaxDataDiskCount'; })[0].value; + const displayText = `${value.name} Cores: ${cores} Memory: ${memory}GB discCount: ${discCount} discSize: ${discSize}GB`; + this._vmSize.push(displayText); + return { + name: value.name, + displayName: displayText + }; + } + return; + }); + + dropDownValues.sort((a, b) => (a!.displayName > b!.displayName) ? 1 : -1); + + this._vmSize = []; + + this._vmSizeDropdown.updateProperties({ + values: dropDownValues, + value: dropDownValues[0], + width: '480px' + }); + this.wizard.model.vmSize = (this._vmSizeDropdown.value as azdata.CategoryValue).name; + this._vmSizeDropdown.loading = false; + } + + protected async validatePage(): Promise { + + const errorMessages = []; + /** + * VM name rules: + * 1. 1-15 characters + * 2. Cannot contain only numbers + * 3. Cannot start with underscore and end with period or hyphen + * 4. Virtual machine name cannot contain special characters \/""[]:|<>+=;,?* + */ + let vmname = this.wizard.model.vmName; + if (vmname.length < 1 && vmname.length > 15) { + errorMessages.push(localize('deployAzureSQLVM.VnameLengthError', "Virtual machine name must be between 1 and 15 characters long.")); + } + if (/^\d+$/.test(vmname)) { + errorMessages.push(localize('deployAzureSQLVM.VNameOnlyNumericNameError', "Virtual machine name cannot contain only numbers.")); + } + if (vmname.charAt(0) === '_' || vmname.slice(-1) === '.' || vmname.slice(-1) === '-') { + errorMessages.push(localize('deployAzureSQLVM.VNamePrefixSuffixError', "Virtual machine name Can\'t start with underscore. Can\'t end with period or hyphen")); + } + if (/[\\\/"\'\[\]:\|<>\+=;,\?\*@\&,]/g.test(vmname)) { + errorMessages.push(localize('deployAzureSQLVM.VNameSpecialCharError', "Virtual machine name cannot contain special characters \/\"\"[]:|<>+=;,?*@&, .")); + } + if (await this.vmNameExists(vmname)) { + errorMessages.push(localize('deployAzureSQLVM.VNameExistsError', "Virtual machine name must be unique in the current resource group.")); + } + + + /** + * VM admin/root username rules: + * 1. 1-20 characters long + * 2. cannot contain special characters \/""[]:|<>+=;,?* + */ + const reservedVMUsernames: string[] = [ + 'administrator', 'admin', 'user', 'user1', 'test', 'user2', + 'test1', 'user3', 'admin1', '1', '123', 'a', 'actuser', 'adm', 'admin2', + 'aspnet', 'backup', 'console', 'david', 'guest', 'john', 'owner', 'root', 'server', 'sql', 'support', + 'support_388945a0', 'sys', 'test2', 'test3', 'user4', 'user5' + ]; + let username = this.wizard.model.vmUsername; + if (username.length < 1 || username.length > 20) { + errorMessages.push(localize('deployAzureSQLVM.VMUsernameLengthError', "Username must be between 1 and 20 characters long.")); + } + if (username.slice(-1) === '.') { + errorMessages.push(localize('deployAzureSQLVM.VMUsernameSuffixError', 'Username cannot end with period')); + } + if (/[\\\/"\'\[\]:\|<>\+=;,\?\*@\&]/g.test(username)) { + errorMessages.push(localize('deployAzureSQLVM.VMUsernameSpecialCharError', "Username cannot contain special characters \/\"\"[]:|<>+=;,?*@& .")); + } + + if (reservedVMUsernames.includes(username)) { + errorMessages.push(localize('deployAzureSQLVM.VMUsernameReservedWordsError', "Username must not include reserved words.")); + } + + errorMessages.push(this.wizard.validatePassword(this.wizard.model.vmPassword)); + + if (this.wizard.model.vmPassword !== this._adminComfirmPasswordTextBox.value) { + errorMessages.push(localize('deployAzureSQLVM.VMConfirmPasswordError', "Password and confirm password must match.")); + } + + if (this._vmSize.includes((this._vmSizeDropdown.value as azdata.CategoryValue).name)) { + errorMessages.push(localize('deployAzureSQLVM.vmDropdownSizeError', "Select a valid virtual machine size.")); + } + + this.wizard.showErrorMessage(errorMessages.join(EOL)); + + return errorMessages.join(EOL); + } + + protected async vmNameExists(vmName: string): Promise { + const url = `https://management.azure.com` + + `/subscriptions/${this.wizard.model.azureSubscription}` + + `/resourceGroups/${this.wizard.model.azureResouceGroup}` + + `/providers/Microsoft.Compute` + + `/virtualMachines?api-version=2019-12-01`; + + let response = await this.wizard.getRequest(url, true); + + let nameArray = response.data.value.map((v: any) => { return v.name; }); + return (nameArray.includes(vmName)); + + } + + + +}