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 @@
+
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));
+
+ }
+
+
+
+}