From e60b01ac005f18e632fe9da6d9da12f8d88ebefa Mon Sep 17 00:00:00 2001 From: Aasim Khan Date: Sun, 11 Oct 2020 13:06:41 -0700 Subject: [PATCH] Adding sql vm and sql db notebooks to october (#12880) * SQL VM deployments (#12144) * Added sql vm deployment option * Added more fields for sql vm deployments * created basic sqlvm deployment. Mostly hardcoded * added string to package.nls * added poc deployments for sql vm * Made some changes in the notebook that was mentioned in PR * Added scaffolding for azure sql vm wizard. * code cleanups * added some async logic * added loading component * fixed loader code * completed page2 of wizard * added some more required fields. * added some more fields * added network settings page * added sql server settings page * added azure signin support and sql server settings page * added some helper methods in wizard code * added some fixes * fixed azure and vm setting page added validation in azure setting page * added changes for the notebook variable * validations and other bug fixes * commenting sql storage optimization dropdown * cleanedup wizard base page * reversing vm image list to display newer images first * cleaning model code * added validations for network setting * Completed summary page fixed the code poisition some additional field validations * fixed networking page * - fixed an error with vm size model variable - removed byol images because it was not working with az sql vm - Fixed vm size display names in dropdown * added double quotes to some localized strings * added some space inside strings * -Added live validations -Restyled network component -Added required to regions -Some bug fixes * -redesigned summary page -localized some strings * Fixed summary page section titles * -Fixed validations on sql server settings page -Fixed some fields on Summary Page * corrected onleave validation using array for error messages using Promises.all * Fixed bug on network settings dropdowns when user does not have existing resource to populate them * Change resource deployment display name Added Ninar's iteration of the notebook Changed RDP check box label Surfacing API errors to user Filtering regions based on Azure VM regions and user's subscription region Made form validation async Displaying new checkbox on network page when dropdowns empty Fixed a small bug in SQL auth form validation Made summary single item per row and fixed the gaps in spacing Fixed validations in vm page Checking if vm name already exists on azure * Fixed sql vm eula Fixed sql vm description Added hyperlink for more info on vm sizes * Replaced loading component with dropdown loaders. * localized string Fixed a bug in network settings page * Added additonal filtering * added reverse to image images * Fixing some merge related issues * Fixed conflicts * sql db deployments into main (WIP) (#12767) * added my resource-deployment * changed notebook message * Add more advanced properties for spark job submission dialog (#12732) * Add more advanced properties for spark job submission dialog * Add queue * Revert "Add more advanced properties for spark job submission dialog (#12732)" This reverts commit e6a7e86ddbe70b39660098a8ebd9ded2a1c5530c. * Changes made for simplification * changed error messages * tags added * tags removed due to redundancy * Update package.json * Update resourceTypePickerDialog.ts * changes based on feedback * activaterealtimevalidation removed Co-authored-by: Charles Gagnon * adding tags to sql vm * added register navigation for Azure settings page * simplified check Co-authored-by: Alex Ma Co-authored-by: Charles Gagnon --- .../images/azure-sql-vm.svg | 25 + .../notebooks/azuredb/create-sqldb.ipynb | 211 +++++ .../notebooks/azurevm/create-sqlvm.ipynb | 396 +++++++++ extensions/resource-deployment/package.json | 120 ++- .../resource-deployment/package.nls.json | 36 +- .../resource-deployment/src/interfaces.ts | 36 +- .../src/services/resourceTypeService.ts | 37 +- .../ui/deployAzureSQLDBWizard/constants.ts | 45 + .../deployAzureSQLDBWizard.ts | 175 ++++ .../deployAzureSQLDBWizardModel.ts | 56 ++ .../pages/azureSettingsPage.ts | 770 ++++++++++++++++++ .../deployAzureSQLDBWizard/pages/basePage.ts | 11 + .../pages/databaseSettingsPage.ts | 237 ++++++ .../pages/summaryPage.ts | 225 +++++ .../ui/deployAzureSQLVMWizard/constants.ts | 53 ++ .../deployAzureSQLVMWizard.ts | 224 +++++ .../deployAzureSQLVMWizardModel.ts | 74 ++ .../pages/azureSettingsPage.ts | 302 +++++++ .../deployAzureSQLVMWizard/pages/basePage.ts | 26 + .../pages/networkSettingsPage.ts | 478 +++++++++++ .../pages/sqlServerSettingsPage.ts | 249 ++++++ .../pages/summaryPage.ts | 367 +++++++++ .../pages/vmSettingsPage.ts | 467 +++++++++++ .../src/ui/resourceTypePickerDialog.ts | 28 +- 24 files changed, 4632 insertions(+), 16 deletions(-) create mode 100644 extensions/resource-deployment/images/azure-sql-vm.svg create mode 100644 extensions/resource-deployment/notebooks/azuredb/create-sqldb.ipynb create mode 100644 extensions/resource-deployment/notebooks/azurevm/create-sqlvm.ipynb create mode 100644 extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/constants.ts create mode 100644 extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/deployAzureSQLDBWizard.ts create mode 100644 extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/deployAzureSQLDBWizardModel.ts create mode 100644 extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/pages/azureSettingsPage.ts create mode 100644 extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/pages/basePage.ts create mode 100644 extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/pages/databaseSettingsPage.ts create mode 100644 extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/pages/summaryPage.ts create mode 100644 extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/constants.ts create mode 100644 extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/deployAzureSQLVMWizard.ts create mode 100644 extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/deployAzureSQLVMWizardModel.ts create mode 100644 extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/pages/azureSettingsPage.ts create mode 100644 extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/pages/basePage.ts create mode 100644 extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/pages/networkSettingsPage.ts create mode 100644 extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/pages/sqlServerSettingsPage.ts create mode 100644 extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/pages/summaryPage.ts create mode 100644 extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/pages/vmSettingsPage.ts diff --git a/extensions/resource-deployment/images/azure-sql-vm.svg b/extensions/resource-deployment/images/azure-sql-vm.svg new file mode 100644 index 0000000000..2676d16464 --- /dev/null +++ b/extensions/resource-deployment/images/azure-sql-vm.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + Icon-databases-124 + + + + + + + + diff --git a/extensions/resource-deployment/notebooks/azuredb/create-sqldb.ipynb b/extensions/resource-deployment/notebooks/azuredb/create-sqldb.ipynb new file mode 100644 index 0000000000..8d51d66908 --- /dev/null +++ b/extensions/resource-deployment/notebooks/azuredb/create-sqldb.ipynb @@ -0,0 +1,211 @@ +{ + "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 Database\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. Provision firewall rules to allow local access\n", + "1. Create SQL database resource" + ], + "metadata": { + "azdata_cell_guid": "6af59d69-ade7-480a-b33e-52a86fe5bfd3" + } + }, + { + "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": "b57c46c8-4a34-49af-9b62-aa5688a02002" + } + }, + { + "cell_type": "markdown", + "source": [ + "## Notebook Setup " + ], + "metadata": { + "azdata_cell_guid": "19ebf0fd-7010-4cd6-8bcd-d2f63dc75cfb" + } + }, + { + "cell_type": "code", + "source": [ + "import sys, os, json, time, string, random, subprocess\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", + ], + "metadata": { + "azdata_cell_guid": "c320ffe2-c488-4bd8-9886-c7deeae02996", + "tags": [] + }, + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Connecting to your Azure account\r\n", + "" + ], + "metadata": { + "azdata_cell_guid": "e34334a7-0d55-4c18-8c0a-1c4a673629cd" + } + }, + { + "cell_type": "code", + "source": [ + "subscriptions = run_command('az account list', printOutput = False)\r\n", + "if azure_sqldb_subscription not in (subscription[\"id\"] for subscription in subscriptions):\r\n", + " run_command('az login')" + ], + "metadata": { + "azdata_cell_guid": "96800b54-48a8-463b-886c-3d0e96f29765" + }, + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Setting your Azure subscription\r\n", + "" + ], + "metadata": { + "azdata_cell_guid": "ed6b781d-ce7e-4b51-a7ec-1eeeb2032c73" + } + }, + { + "cell_type": "code", + "source": [ + "run_command(\r\n", + " 'az account set '\r\n", + " '--subscription {0}'\r\n", + " .format(\r\n", + " azure_sqldb_subscription));" + ], + "metadata": { + "azdata_cell_guid": "17b57956-98cf-44de-9ab5-348469ddabf4" + }, + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Create a server firewall rule\r\n", + "\r\n", + "This firewall rule will allow you to access your server and database within IP range immediately after it is created." + ], + "metadata": { + "azdata_cell_guid": "ba895abf-3176-48b5-9e49-a060b3f74370" + } + }, + { + "cell_type": "code", + "source": [ + "create_firewall_rule_result = run_command(\r\n", + " 'az sql server firewall-rule create '\r\n", + " '--start-ip-address {0} '\r\n", + " '--end-ip-address {1} '\r\n", + " '--server {2} '\r\n", + " '--name {3} '\r\n", + " '--resource-group {4} '\r\n", + " .format(\r\n", + " azure_sqldb_ip_start, \r\n", + " azure_sqldb_ip_end, \r\n", + " azure_sqldb_server_name, \r\n", + " azure_sqldb_firewall_name, \r\n", + " azure_sqldb_resource_group_name));" + ], + "metadata": { + "azdata_cell_guid": "ceae5670-292f-4c45-9c10-4ac85baf2d07" + }, + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## Create Azure SQL Database\r\n", + "\r\n", + "The database will be created based on all the settings previously specified. [Learn more](https://docs.microsoft.com/en-us/cli/azure/sql/db?view=azure-cli-latest#az_sql_db_create) about additonal options for creating the database." + ], + "metadata": { + "azdata_cell_guid": "b460ca8f-65a7-4d6c-94b7-6d7dd9655fad" + } + }, + { + "cell_type": "code", + "source": [ + "create_database_result = run_command(\r\n", + " 'az sql db create '\r\n", + " '--server {0} '\r\n", + " '--name {1} '\r\n", + " '--edition GeneralPurpose '\r\n", + " '--compute-model Serverless '\r\n", + " '--family Gen5 '\r\n", + " '--resource-group {2} '\r\n", + " '--min-capacity 0.5 '\r\n", + " '--max-size 32GB '\r\n", + " '--capacity 1 '\r\n", + " '--collation {3} '\r\n", + " .format(\r\n", + " azure_sqldb_server_name, \r\n", + " azure_sqldb_database_name, \r\n", + " azure_sqldb_resource_group_name, \r\n", + " azure_sqldb_collation));" + ], + "metadata": { + "azdata_cell_guid": "dc3b2f6f-83ac-4a4d-9d81-2f534e90913e" + }, + "outputs": [], + "execution_count": null + } + ] +} 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 932774e008..415acf3093 100644 --- a/extensions/resource-deployment/package.json +++ b/extensions/resource-deployment/package.json @@ -401,19 +401,127 @@ "displayName": "%azure-sql-displayName%", "description": "%azure-sql-description%", "platforms": "*", - "okButtonText": "%azure-sql-ok-button-text%", "icon": { "light": "./images/azure-sql.svg", "dark": "./images/azure-sql.svg" }, - "tags": ["Cloud"], - "options": [], + "tags": ["SQL Server", "Cloud"], + "okButtonText": [ + { + "value": "%azure-sqldb-notebook-ok-button-text%", + "when": "resource-type=single-database" + }, + { + "value": "%azure-sqldb-portal-ok-button-text%", + "when": "resource-type=elastic-pool" + }, + { + "value": "%azure-sqldb-portal-ok-button-text%", + "when": "resource-type=database-server" + } + ], + "options": [ + { + "name": "resource-type", + "displayName": "%resource-type-display-name%", + "values": [ + { + "name": "single-database", + "displayName": "%sql-azure-single-database-display-name%" + }, + { + "name": "elastic-pool", + "displayName": "%sql-azure-elastic-pool-display-name%" + }, + { + "name": "database-server", + "displayName": "%sql-azure-database-server-display-name%" + } + ] + } + ], "providers": [ { - "webPageUrl": "https://portal.azure.com/#create/Microsoft.AzureSQL", - "requiredTools": [] + "azureSQLDBWizard":{ + "notebook": "./notebooks/azuredb/create-sqldb.ipynb" + }, + "requiredTools": [ + { + "name": "azure-cli" + } + ], + "when": "resource-type=single-database" + }, + { + "webPageUrl": "https://portal.azure.com/#create/Microsoft.SQLElasticDatabasePool", + "requiredTools": [], + "when": "resource-type=elastic-pool" + }, + { + "webPageUrl": "https://portal.azure.com/#create/Microsoft.SQLServer", + "requiredTools": [], + "when": "resource-type=database-server" } - ] + ], + "agreement": { + "template": "%azure-sqldb-agreement%", + "links": [ + { + "text": "%microsoft-privacy-statement%", + "url": "https://go.microsoft.com/fwlink/?LinkId=853010" + }, + { + "text": "%azure-sqldb-agreement-sqldb-eula%", + "url": "https://azure.microsoft.com/support/legal/" + }, + { + "text": "%azure-sqldb-agreement-azdata-eula%", + "url": "https://aka.ms/eula-azdata-en" + } + ] + } + }, + { + "name": "azure-sql-vm", + "displayName": "%azure-sqlvm-display-name%", + "description": "%azure-sqlvm-description%", + "platforms": "*", + "displayIndex": 5, + "icon": { + "light": "./images/azure-sql-vm.svg", + "dark": "./images/azure-sql-vm.svg" + }, + "tags": ["SQL Server", "Cloud"], + "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 f084f39f10..bbcf0afcef 100644 --- a/extensions/resource-deployment/package.nls.json +++ b/extensions/resource-deployment/package.nls.json @@ -49,5 +49,39 @@ "azdata-install-location-description": "Location of the azdata package used for the install command", "azure-sql-displayName":"Azure SQL", "azure-sql-description":"Create a SQL Database, SQL Virtual Machine, or SQL Managed Instance by going to the Azure portal", - "azure-sql-ok-button-text": "Create in Azure portal" + "azure-sql-ok-button-text": "Create in Azure portal", + "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": "SQL Database on Azure Server", + "azure-sqldb-description": "Create SQL Databases on Azure. Best for new applications or existing on-premises applications.", + "azure-sqldb-portal-ok-button-text": "Create in Azure portal", + "azure-sqldb-notebook-ok-button-text": "Script to Notebook", + "resource-type-display-name": "Resource Type", + "sql-azure-single-database-display-name": "Single Database", + "sql-azure-elastic-pool-display-name": "Elastic Pool", + "sql-azure-database-server-display-name": "Database Server", + "azure-sqldb-agreement": "I accept {0}, {1} and {2}.", + "azure-sqldb-agreement-sqldb-eula": "Azure SQL DB License Terms", + "azure-sqldb-agreement-azdata-eula": "azdata License Terms" } diff --git a/extensions/resource-deployment/src/interfaces.ts b/extensions/resource-deployment/src/interfaces.ts index a7bba7028c..c0940fcee5 100644 --- a/extensions/resource-deployment/src/interfaces.ts +++ b/extensions/resource-deployment/src/interfaces.ts @@ -19,8 +19,9 @@ export interface ResourceType { providers: DeploymentProvider[]; agreement?: AgreementInfo; displayIndex?: number; + okButtonText?: OkButtonTextValue[]; + getOkButtonText(selectedOptions: { option: string, value: string }[]): string | undefined; getProvider(selectedOptions: { option: string, value: string }[]): DeploymentProvider | undefined; - okButtonText?: string; tags?: string[]; } @@ -40,6 +41,11 @@ export interface ResourceTypeOptionValue { displayName: string; } +export interface OkButtonTextValue { + value: string; + when: string; +} + export interface DialogDeploymentProvider extends DeploymentProviderBase { dialog: DialogInfo; } @@ -68,6 +74,14 @@ export interface CommandDeploymentProvider extends DeploymentProviderBase { command: string; } +export interface AzureSQLVMDeploymentProvider extends DeploymentProviderBase { + azureSQLVMWizard: AzureSQLVMWizardInfo; +} + +export interface AzureSQLDBDeploymentProvider extends DeploymentProviderBase { + azureSQLDBWizard: AzureSQLDBWizardInfo; +} + export function instanceOfDialogDeploymentProvider(obj: any): obj is DialogDeploymentProvider { return obj && 'dialog' in obj; } @@ -96,12 +110,20 @@ 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 function instanceOfAzureSQLDBDeploymentProvider(obj: any): obj is AzureSQLDBDeploymentProvider { + return obj && 'azureSQLDBWizard' 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 | AzureSQLDBDeploymentProvider; export interface BdcWizardInfo { notebook: string | NotebookPathInfo; @@ -170,6 +192,14 @@ export interface CommandBasedDialogInfo extends DialogInfoBase { command: string; } +export interface AzureSQLVMWizardInfo { + notebook: string | NotebookPathInfo; +} + +export interface AzureSQLDBWizardInfo { + notebook: string | NotebookPathInfo; +} + export type DialogInfo = NotebookBasedDialogInfo | CommandBasedDialogInfo; export function instanceOfNotebookBasedDialogInfo(obj: any): obj is NotebookBasedDialogInfo { @@ -270,6 +300,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 1a264d8078..461021343f 100644 --- a/extensions/resource-deployment/src/services/resourceTypeService.ts +++ b/extensions/resource-deployment/src/services/resourceTypeService.ts @@ -10,7 +10,8 @@ import * as os from 'os'; import * as path from 'path'; import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; -import { DeploymentProvider, instanceOfCommandDeploymentProvider, instanceOfDialogDeploymentProvider, instanceOfDownloadDeploymentProvider, instanceOfNotebookBasedDialogInfo, instanceOfNotebookDeploymentProvider, instanceOfNotebookWizardDeploymentProvider, instanceOfWebPageDeploymentProvider, instanceOfWizardDeploymentProvider, NotebookInfo, NotebookPathInfo, ResourceType, ResourceTypeOption } from '../interfaces'; +import { DeploymentProvider, instanceOfAzureSQLVMDeploymentProvider, instanceOfAzureSQLDBDeploymentProvider, instanceOfCommandDeploymentProvider, instanceOfDialogDeploymentProvider, instanceOfDownloadDeploymentProvider, instanceOfNotebookBasedDialogInfo, instanceOfNotebookDeploymentProvider, instanceOfNotebookWizardDeploymentProvider, instanceOfWebPageDeploymentProvider, instanceOfWizardDeploymentProvider, NotebookInfo, NotebookPathInfo, ResourceType, ResourceTypeOption } from '../interfaces'; +import { DeployAzureSQLDBWizard } from '../ui/deployAzureSQLDBWizard/deployAzureSQLDBWizard'; import { DeployClusterWizard } from '../ui/deployClusterWizard/deployClusterWizard'; import { DeploymentInputDialog } from '../ui/deploymentInputDialog'; import { NotebookWizard } from '../ui/notebookWizard/notebookWizard'; @@ -19,7 +20,9 @@ import { KubeService } from './kubeService'; import { INotebookService } from './notebookService'; import { IPlatformService } from './platformService'; import { IToolsService } from './toolsService'; +import * as loc from './../localizedConstants'; +import { DeployAzureSQLVMWizard } from '../ui/deployAzureSQLVMWizard/deployAzureSQLVMWizard'; const localize = nls.loadMessageBundle(); export interface IResourceTypeService { @@ -45,6 +48,7 @@ export class ResourceTypeService implements IResourceTypeService { extensionResourceTypes.forEach((resourceType: ResourceType) => { this.updatePathProperties(resourceType, extension.extensionPath); resourceType.getProvider = (selectedOptions) => { return this.getProvider(resourceType, selectedOptions); }; + resourceType.getOkButtonText = (selectedOptions) => { return this.getOkButtonText(resourceType, selectedOptions); }; this._resourceTypes.push(resourceType); }); } @@ -74,6 +78,12 @@ 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); + } + else if ('azureSQLDBWizard' in provider) { + this.updateNotebookPath(provider.azureSQLDBWizard, extensionPath); + } }); } @@ -182,7 +192,9 @@ export class ResourceTypeService implements IResourceTypeService { && !instanceOfNotebookDeploymentProvider(provider) && !instanceOfDownloadDeploymentProvider(provider) && !instanceOfWebPageDeploymentProvider(provider) - && !instanceOfCommandDeploymentProvider(provider)) { + && !instanceOfCommandDeploymentProvider(provider) + && !instanceOfAzureSQLVMDeploymentProvider(provider) + && !instanceOfAzureSQLDBDeploymentProvider(provider)) { errorMessages.push(`No deployment method defined for the provider, ${providerPositionInfo}`); } @@ -240,6 +252,21 @@ export class ResourceTypeService implements IResourceTypeService { return undefined; } + /** + * Get the ok button text based on the selected options + */ + private getOkButtonText(resourceType: ResourceType, selectedOptions: { option: string, value: string }[]): string | undefined { + if (resourceType.okButtonText && selectedOptions.length === 1) { + const optionGiven = `${selectedOptions[0].option}=${selectedOptions[0].value}`; + for (const possibleOption of resourceType.okButtonText) { + if (possibleOption.when === optionGiven || possibleOption.when === undefined || possibleOption.when.toString().toLowerCase() === 'true') { + return possibleOption.value; + } + } + } + return loc.select; + } + public startDeployment(provider: DeploymentProvider): void { const self = this; if (instanceOfWizardDeploymentProvider(provider)) { @@ -275,6 +302,12 @@ 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(); + } else if (instanceOfAzureSQLDBDeploymentProvider(provider)) { + const wizard = new DeployAzureSQLDBWizard(provider.azureSQLDBWizard, this.notebookService, this.toolsService); + wizard.open(); } } diff --git a/extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/constants.ts b/extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/constants.ts new file mode 100644 index 0000000000..02bcf1b8e5 --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/constants.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * 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 DB wizard constants +export const WizardTitle = localize('deployAzureSQLDB.NewSQLDBTitle', "Deploy Azure SQL DB"); +export const WizardDoneButtonLabel = localize('deployAzureSQLDB.ScriptToNotebook', "Script to Notebook"); +export const MissingRequiredInformationErrorMessage = localize('deployAzureSQLDB.MissingRequiredInfoError', "Please fill out the required fields marked with red asterisks."); + +// Azure settings page constants +export const AzureSettingsPageTitle = localize('deployAzureSQLDB.AzureSettingsPageTitle', "Azure SQL Database - Azure Account Settings"); +export const AzureSettingsSummaryPageTitle = localize('deployAzureSQLDB.AzureSettingsSummaryPageTitle', "Azure Account Settings"); +export const AzureAccountDropdownLabel = localize('deployAzureSQLDB.AzureAccountDropdownLabel', "Azure Account"); +export const AzureAccountSubscriptionDropdownLabel = localize('deployAzureSQLDB.AzureSubscriptionDropdownLabel', "Subscription"); +export const AzureAccountDatabaseServersDropdownLabel = localize('deployAzureSQLDB.AzureDatabaseServersDropdownLabel', "Server"); +export const AzureAccountResourceGroupDropdownLabel = localize('deployAzureSQLDB.ResourceGroup', "Resource Group"); +//@todo alma1 9/8/20 Region label used for upcoming server creation feature. +//export const AzureAccountRegionDropdownLabel = localize('deployAzureSQLDB.AzureRegionDropdownLabel', "Region (for Public IP Address)"); + +//Azure settings Database hardware properties. //@todo alma1 9/8/20 labels used for upcoming database hardware creation feature. +// export const DatabaseHardwareInfoLabel = localize('deployAzureSQLDB.DatabaseHardwareInfo', "SQLDB Hardware Settings"); +// export const DatabaseManagedInstanceDropdownLabel = localize('deployAzureSQLDB.DatabaseManagedInstanceDropdownLabel', "SQLDB Version"); +// export const DatabaseSupportedEditionsDropdownLabel = localize('deployAzureSQLDB.DatabaseSupportedEditionsDropdownLabel', "Edition Type"); +// export const DatabaseSupportedFamilyDropdownLabel = localize('deployAzureSQLDB.DatabaseSupportedFamilyDropdownLabel', "Family Type"); +// export const DatabaseVCoreNumberDropdownLabel = localize('deployAzureSQLDB.DatabaseVCoreNumberDropdownLabel', "Number of Vcores"); +// export const DatabaseMaxMemoryTextLabel = localize('deployAzureSQLDB.DatabaseMaxMemoryTextLabel', "Maximum Data Storage Capacity in GB, can go up to 1TB (1024 GB)."); +// export const DatabaseMaxMemorySummaryTextLabel = localize('deployAzureSQLDB.DatabaseMaxMemorySummaryTextLabel', "Maximum Data Storage Capacity in GB"); + +// Database settings page constants +export const DatabaseSettingsPageTitle = localize('deployAzureSQLDB.DatabaseSettingsPageTitle', "Database settings"); +export const FirewallRuleNameLabel = localize('deployAzureSQLDB.FirewallRuleNameLabel', "Firewall rule name"); +export const DatabaseNameLabel = localize('deployAzureSQLDB.DatabaseNameLabel', "SQL database name"); +export const CollationNameLabel = localize('deployAzureSQLDB.CollationNameLabel', "Database collation"); +export const CollationNameSummaryLabel = localize('deployAzureSQLDB.CollationNameSummaryLabel', "Collation for database"); +export const IpAddressInfoLabel = localize('deployAzureSQLDB.IpAddressInfoLabel', "Enter IP Addresses in IPv4 format."); +export const StartIpAddressLabel = localize('deployAzureSQLDB.StartIpAddressLabel', "Min IP Address in firewall Ip Range"); +export const EndIpAddressLabel = localize('deployAzureSQLDB.EndIpAddressLabel', "Max IP Address in firewall IP Range"); +export const StartIpAddressShortLabel = localize('deployAzureSQLDB.StartIpAddressShortLabel', "Min IP Address"); +export const EndIpAddressShortLabel = localize('deployAzureSQLDB.EndIpAddressShortLabel', "Max IP Address"); diff --git a/extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/deployAzureSQLDBWizard.ts b/extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/deployAzureSQLDBWizard.ts new file mode 100644 index 0000000000..d3c23d7a9a --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/deployAzureSQLDBWizard.ts @@ -0,0 +1,175 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { DeployAzureSQLDBWizardModel } from './deployAzureSQLDBWizardModel'; +import { AzureSQLDBWizardInfo } from '../../interfaces'; +import { AzureSettingsPage } from './pages/azureSettingsPage'; +import { DatabaseSettingsPage } from './pages/databaseSettingsPage'; +import axios, { AxiosRequestConfig } from 'axios'; +import { AzureSQLDBSummaryPage } from './pages/summaryPage'; +import { EOL } from 'os'; + +export class DeployAzureSQLDBWizard extends WizardBase, DeployAzureSQLDBWizardModel> { + + constructor(private wizardInfo: AzureSQLDBWizardInfo, private _notebookService: INotebookService, private _toolsService: IToolsService) { + super( + constants.WizardTitle, + 'DeployAzureSqlDBWizard', + new DeployAzureSQLDBWizardModel(), + _toolsService + ); + } + + private cache: Map = new Map(); + + 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 DatabaseSettingsPage(this)); + pages.push(new AzureSQLDBSummaryPage(this)); + return pages; + } + + private async scriptToNotebook(): Promise { + const variableValueStatements = this.model.getCodeCellContentForNotebook(); + const insertionPosition = 2; // Cell number 2 is the position where the python variable setting statements need to be inserted in this.wizardInfo.notebook. + try { + await this.notebookService.openNotebookWithEdits(this.wizardInfo.notebook, variableValueStatements, insertionPosition); + } catch (error) { + vscode.window.showErrorMessage(error); + } + } + + + 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 + }; + } +} diff --git a/extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/deployAzureSQLDBWizardModel.ts b/extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/deployAzureSQLDBWizardModel.ts new file mode 100644 index 0000000000..58bc590c9d --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/deployAzureSQLDBWizardModel.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * 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 DeployAzureSQLDBWizardModel extends Model { + public azureAccount!: azdata.Account; + public securityToken!: any; + public azureSubscription!: string; + public azureSubscriptionDisplayName!: string; + public azureResouceGroup!: string; + public azureServerName!: string; + public azureRegion!: string; + + // public databaseEdition!: string; //@todo alma1 10/7/2020 used for upcoming database hardware creation feature + // public databaseFamily!: string; + // public vCoreNumber!: number; + // public storageInGB!: string; + + public databaseName!: string; + //public newServer!: 'True' | 'False'; //@todo alma1 9/8/2020 used for upcoming server creation feature. + public startIpAddress!: string; + public endIpAddress!: string; + public firewallRuleName!: string; + public databaseCollation!: string; + + + constructor() { + super(); + } + + public getCodeCellContentForNotebook(): string[] { + const statements: string[] = []; + + statements.push(`azure_sqldb_subscription = '${this.azureSubscription}'`); + statements.push(`azure_sqldb_resource_group_name = '${this.azureResouceGroup}'`); + statements.push(`azure_sqldb_server_name = '${this.azureServerName}'`); + //statements.push(`azure_sqldb_database_edition = '${this.databaseEdition}'`); //@todo alma1 10/7/2020 used for upcoming datbase hardware creation feature. + statements.push(`azure_sqldb_database_name = '${this.databaseName}'`); + //statements.push(`azure_sqldb_location = '${this.azureRegion}'`); //@todo alma1 9/10/2020 used for upcoming server creation feature. + statements.push(`azure_sqldb_ip_start = '${this.startIpAddress}'`); + statements.push(`azure_sqldb_ip_end = '${this.endIpAddress}'`); + statements.push(`azure_sqldb_firewall_name = '${this.firewallRuleName}'`); + statements.push(`azure_sqldb_collation = '${this.databaseCollation}'`); + // statements.push(`azure_sqldb_family = '${this.databaseFamily}'`); //@todo alma1 10/7/2020 used for upcoming datbase hardware creation feature. + // statements.push(`azure_sqldb_vcore = '${this.vCoreNumber}'`); + // statements.push(`azure_sqldb_maxmemory = '${this.storageInGB}'`); + //statements.push(`azure_sqldb_new_server = '${this.newServer}'`); //@todo alma1 9/8/2020 used for upcoming server creation feature. + + return statements.map(line => line.concat(EOL)); + } +} diff --git a/extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/pages/azureSettingsPage.ts b/extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/pages/azureSettingsPage.ts new file mode 100644 index 0000000000..7d5f7b96bd --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/pages/azureSettingsPage.ts @@ -0,0 +1,770 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { DeployAzureSQLDBWizard } from '../deployAzureSQLDBWizard'; +import { apiService } from '../../../services/apiService'; +import { azureResource } from 'azureResource'; +import * as vscode from 'vscode'; +import { BasePage } from './basePage'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +export class AzureSettingsPage extends BasePage { + // <- 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 //@todo alma1 9/9/2020 Used for upcoming server creation feature. + // private _resourceGroupDropdown!: azdata.DropDownComponent; + + //dropdown for SQL servers <- subscription dropdown + private _serverGroupDropdown!: azdata.DropDownComponent; + + // //dropdown for azure regions <- subscription dropdown //@todo alma1 9/8/2020 Region dropdown used for upcoming server creation feature. + // private _azureRegionsDropdown!: azdata.DropDownComponent; + + // //information text about hardware settings. //@todo alma1 9/8/2020 components below are used for upcoming database hardware creation feature. + // private _dbHardwareInfoText!: azdata.TextComponent; + + // //dropdown for Managed Instance Versions <- server dropdown. + // private _dbManagedInstanceDropdown!: azdata.DropDownComponent; + + // //dropdown for Supported Editions <- Managed Instance dropdown. + // private _dbSupportedEditionsDropdown!: azdata.DropDownComponent; + + // //dropdown for Supported Family <- Supported Editions dropdown. + // private _dbSupportedFamilyDropdown!: azdata.DropDownComponent; + + // //dropdown for VCore <= Supported Family dropdown. + // private _dbVCoreDropdown!: azdata.DropDownComponent; + + + // //input box for maximum memory size, supports between 1 and 1024 GB (1 TB) + // private _dbMemoryTextBox!: azdata.InputBoxComponent; + + private _form!: azdata.FormContainer; + + private _accountsMap!: Map; + private _subscriptionsMap!: Map; + constructor(wizard: DeployAzureSQLDBWizard) { + 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), //@todo alma1 9/8/2020 used for upcoming server creation feature. + this.createServerDropdown(view), + //this.createAzureRegionsDropdown(view) //@todo alma1 9/8/2020 used for upcoming server creation feature. + // this.createDatabaseHardwareSettingsText(view), //@todo alma1 9/8/2020 used for upcoming database hardware creation feature. + // this.createManagedInstanceDropdown(view), + // this.createSupportedEditionsDropdown(view), + // this.createSupportedFamilyDropdown(view), + // this.createVCoreDropdown(view), + // this.createMaxMemoryText(view), + ]); + this.populateAzureAccountsDropdown(); + + this.registerValidator(); + + 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) + }, + // { //@todo alma1 9/9/2020 Used for upcoming server creation feature. + // component: this.wizard.createFormRowComponent(view, constants.AzureAccountResourceGroupDropdownLabel, '', this._resourceGroupDropdown, true) + // }, + { + component: this.wizard.createFormRowComponent(view, constants.AzureAccountDatabaseServersDropdownLabel, '', this._serverGroupDropdown, true) + }, + // { //@todo alma1 9/8/2020 Used for upcoming server creation feature. + // component: this.wizard.createFormRowComponent(view, constants.AzureAccountRegionDropdownLabel, '', this._azureRegionsDropdown, true) + // } + // { //@todo alma1 9/8/2020 Used for upcoming database hardware creation feature. + // component: this._dbHardwareInfoText + // }, + // { + // component: this.wizard.createFormRowComponent(view, constants.DatabaseManagedInstanceDropdownLabel, '', this._dbManagedInstanceDropdown, true) + // }, + // { + // component: this.wizard.createFormRowComponent(view, constants.DatabaseSupportedEditionsDropdownLabel, '', this._dbSupportedEditionsDropdown, true) + // }, + // { + // component: this.wizard.createFormRowComponent(view, constants.DatabaseSupportedFamilyDropdownLabel, '', this._dbSupportedFamilyDropdown, true) + // }, + // { + // component: this.wizard.createFormRowComponent(view, constants.DatabaseVCoreNumberDropdownLabel, '', this._dbVCoreDropdown, true) + // }, + // { + // component: this.wizard.createFormRowComponent(view, constants.DatabaseMaxMemoryTextLabel, '', this._dbMemoryTextBox, true) + // } + ], + { + horizontal: false, + componentWidth: '100%' + }) + .withLayout({ width: '100%' }) + .component(); + return view.initializeModel(this._form); + }); + } + + public async onEnter(): Promise { + this.registerValidator(); + } + + 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(localize('deployAzureSQLDB.azureSignInError', "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().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 + ); + + await this.populateServerGroupDropdown(); + //@todo alma1 9/8/2020 used for upcoming server creation feature. + //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.populateServerGroupDropdown(); + //@todo alma1 9/8/2020 used for upcoming server creation feature. + //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.populateServerGroupDropdown(); + //@todo alma1 9/8/2020 used for upcoming server creation feature. + //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.populateServerGroupDropdown(); + //@todo alma1 9/8/2020 used for upcoming server creation feature. + //await this.populateResourceGroupDropdown(); + //await this.populateAzureRegionsDropdown(); + } + + private async createServerDropdown(view: azdata.ModelView) { + this._serverGroupDropdown = view.modelBuilder.dropDown().withProperties({ + required: true, + }).component(); + this._serverGroupDropdown.onValueChanged(async (value) => { + if (value.selected === ((this._serverGroupDropdown.value as azdata.CategoryValue).displayName)) { + this.wizard.model.azureServerName = value.selected; + this.wizard.model.azureResouceGroup = (this._serverGroupDropdown.value as azdata.CategoryValue).name.replace(RegExp('^(.*?)/resourceGroups/'), '').replace(RegExp('/providers/.*'), ''); + this.wizard.model.azureRegion = (this._serverGroupDropdown.value as azdata.CategoryValue).name.replace(RegExp('^(.*?)/location/'), ''); + //this.populateManagedInstanceDropdown(); //@todo alma1 9/8/2020 functions below are used for upcoming database hardware creation feature. + } + }); + } + + private async populateServerGroupDropdown() { + this._serverGroupDropdown.loading = true; + let currentSubscriptionValue = this._azureSubscriptionsDropdown.value as azdata.CategoryValue; + if (currentSubscriptionValue === undefined || currentSubscriptionValue.displayName === '') { + this._serverGroupDropdown.updateProperties({ + values: [] + }); + this._serverGroupDropdown.loading = false; + // await this.populateManagedInstanceDropdown(); //@todo alma1 9/8/2020 functions below are used for upcoming database hardware creation feature. + return; + } + let url = `https://management.azure.com/subscriptions/${this.wizard.model.azureSubscription}/providers/Microsoft.Sql/servers?api-version=2019-06-01-preview`; + let response = await this.wizard.getRequest(url); + if (response.data.value.length === 0) { + this._serverGroupDropdown.updateProperties({ + values: [ + { + displayName: localize('deployAzureSQLDB.NoServerLabel', "No servers found"), + name: '' + } + ], + }); + this._serverGroupDropdown.loading = false; + // await this.populateManagedInstanceDropdown(); //@todo alma1 9/8/2020 functions below are used for upcoming database hardware creation feature. + return; + } else { + response.data.value.sort((a: azdata.CategoryValue, b: azdata.CategoryValue) => (a!.name > b!.name) ? 1 : -1); + } + this.wizard.addDropdownValues( + this._serverGroupDropdown, + response.data.value.map((value: any) => { + return { + displayName: value.name, + // remove location from this line and others when region population is enabled again. + name: value.id + '/location/' + value.location, + }; + }) + ); + if (this._serverGroupDropdown.value) { + this.wizard.model.azureServerName = (this._serverGroupDropdown.value as azdata.CategoryValue).displayName; + this.wizard.model.azureResouceGroup = (this._serverGroupDropdown.value as azdata.CategoryValue).name.replace(RegExp('^(.*?)/resourceGroups/'), '').replace(RegExp('/providers/.*'), ''); + this.wizard.model.azureRegion = (this._serverGroupDropdown.value as azdata.CategoryValue).name.replace(RegExp('^(.*?)/location/'), ''); + } + this._serverGroupDropdown.loading = false; + // await this.populateManagedInstanceDropdown(); //@todo alma1 9/8/2020 functions below are used for upcoming database hardware creation feature. + return; + } + + //@todo alma1 9/8/2020 functions below are used for upcoming server creation feature. + + // 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; + // this.populateServerGroupDropdown(); + // }); + // } + + // 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; + // await this.populateServerGroupDropdown(); + // 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: [] + // }); + // await this.populateServerGroupDropdown(); + // 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; + // await this.populateServerGroupDropdown(); + // } + + // 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, true); + // 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; + // } + + //@todo alma1 9/8/2020 functions below are used for upcoming database hardware creation feature. + + // private createDatabaseHardwareSettingsText(view: azdata.ModelView) { + // this._dbHardwareInfoText = view.modelBuilder.text() + // .withProperties({ + // value: constants.DatabaseHardwareInfoLabel + // }).component(); + // } + + // private async createManagedInstanceDropdown(view: azdata.ModelView) { + // this._dbManagedInstanceDropdown = view.modelBuilder.dropDown().withProperties({ + // required: true, + // }).component(); + // this._dbManagedInstanceDropdown.onValueChanged(async (value) => { + // this.populateSupportedEditionsDropdown(); + // }); + // } + + // private async populateManagedInstanceDropdown() { + // this._dbManagedInstanceDropdown.loading = true; + // let currentSubscriptionValue = this._azureSubscriptionsDropdown.value as azdata.CategoryValue; + // if (!currentSubscriptionValue || currentSubscriptionValue.displayName === '') { + // this._dbManagedInstanceDropdown.updateProperties({ + // values: [] + // }); + // this._dbManagedInstanceDropdown.loading = false; + // await this.populateSupportedEditionsDropdown(); + // return; + // } + // let currentServerValue = this._serverGroupDropdown.value as azdata.CategoryValue; + + // if (currentServerValue.name === '') { + // this._dbManagedInstanceDropdown.updateProperties({ + // values: [ + // { + // displayName: localize('deployAzureSQLDB.NoServerLabel', "No servers found"), + // name: '', + // supportedEditions: undefined + // } + // ] + // }); + // this._dbManagedInstanceDropdown.loading = false; + // await this.populateSupportedEditionsDropdown(); + // return; + // } + + // let url = `https://management.azure.com/subscriptions/${this.wizard.model.azureSubscription}/providers/Microsoft.Sql/locations/${this.wizard.model.azureRegion}/capabilities?api-version=2017-10-01-preview`; + // let response = await this.wizard.getRequest(url); + + // if (response.data.supportedManagedInstanceVersions.length === 0) { + // this._dbManagedInstanceDropdown.updateProperties({ + // values: [ + // { + // displayName: localize('deployAzureSQLDB.NoHardwareConfigLabel', "No database hardware configuration found"), + // name: '', + // supportedEditions: undefined + // } + // ], + // }); + // this._dbManagedInstanceDropdown.loading = false; + // await this.populateSupportedEditionsDropdown(); + // return; + // } else { + // response.data.supportedManagedInstanceVersions.sort((a: any, b: any) => (a!.name > b!.name) ? 1 : -1); + // } + // this.wizard.addDropdownValues( + // this._dbManagedInstanceDropdown, + // response.data.supportedManagedInstanceVersions.map((value: any) => { + // return { + // displayName: value.name, + // name: value.name, + // supportedEditions: value.supportedEditions + // }; + // }) + // ); + // // if (this._serverGroupDropdown.value) { + // // this.wizard.model.azureServerName = (this._serverGroupDropdown.value as azdata.CategoryValue).displayName; + // // this.wizard.model.azureResouceGroup = (this._serverGroupDropdown.value as azdata.CategoryValue).name.replace(RegExp('^(.*?)/resourceGroups/'), '').replace(RegExp('/providers/.*'), ''); + // // this.wizard.model.azureRegion = (this._serverGroupDropdown.value as azdata.CategoryValue).name.replace(RegExp('^(.*?)/location/'), ''); + // // } + // this._dbManagedInstanceDropdown.loading = false; + // await this.populateSupportedEditionsDropdown(); + // return; + // } + + // private async createSupportedEditionsDropdown(view: azdata.ModelView) { + // this._dbSupportedEditionsDropdown = view.modelBuilder.dropDown().withProperties({ + // required: true, + // }).component(); + // this._dbSupportedEditionsDropdown.onValueChanged(async (value) => { + // this.wizard.model.databaseEdition = value.selected; + // this.populateSupportedFamilyDropdown(); + // }); + // } + + // private async populateSupportedEditionsDropdown() { + // this._dbSupportedEditionsDropdown.loading = true; + // if (!this._dbManagedInstanceDropdown.values || this._dbManagedInstanceDropdown.values!.length === 0) { + // this._dbSupportedEditionsDropdown.updateProperties({ + // values: [] + // }); + // this._dbSupportedEditionsDropdown.loading = false; + // await this.populateSupportedFamilyDropdown(); + // return; + // } + // let currentManagedInstanceValue = this._dbManagedInstanceDropdown.value as any; + // if (!currentManagedInstanceValue.supportedEditions) { + // this._dbSupportedEditionsDropdown.updateProperties({ + // values: [ + // { + // displayName: localize('deployAzureSQLDB.NoManagedInstanceLabel', "Managed instance not selected"), + // name: '' + // } + // ] + // }); + // this._dbSupportedEditionsDropdown.loading = false; + // await this.populateSupportedFamilyDropdown(); + // return; + // } + + // if (currentManagedInstanceValue.supportedEditions.length === 0) { + // this._dbSupportedEditionsDropdown.updateProperties({ + // values: [ + // { + // displayName: localize('deployAzureSQLDB.NoSupportedEditionsLabel', "No supported editions found"), + // name: '' + // } + // ], + // }); + // this._dbSupportedEditionsDropdown.loading = false; + // await this.populateSupportedFamilyDropdown(); + // return; + // } else { + // currentManagedInstanceValue.supportedEditions.sort((a: any, b: any) => (a!.name > b!.name) ? 1 : -1); + // } + // this.wizard.addDropdownValues( + // this._dbSupportedEditionsDropdown, + // currentManagedInstanceValue.supportedEditions.map((value: any) => { + // return { + // displayName: value.name, + // name: value.supportedFamilies + // }; + // }) + // ); + // if (this._dbSupportedEditionsDropdown.value) { + // this.wizard.model.databaseEdition = (this._dbSupportedEditionsDropdown.value as azdata.CategoryValue).displayName; + // } + // this._dbSupportedEditionsDropdown.loading = false; + // await this.populateSupportedFamilyDropdown(); + // return; + // } + + // private async createSupportedFamilyDropdown(view: azdata.ModelView) { + // this._dbSupportedFamilyDropdown = view.modelBuilder.dropDown().withProperties({ + // required: true, + // }).component(); + // this._dbSupportedFamilyDropdown.onValueChanged(async (value) => { + // this.wizard.model.databaseFamily = value.selected; + // this.populateVCoreDropdown(); + // }); + // } + + // private async populateSupportedFamilyDropdown() { + // this._dbSupportedFamilyDropdown.loading = true; + // if (!this._dbSupportedEditionsDropdown.values || this._dbSupportedEditionsDropdown.values!.length === 0) { + // this._dbSupportedFamilyDropdown.updateProperties({ + // values: [] + // }); + // this._dbSupportedFamilyDropdown.loading = false; + // await this.populateVCoreDropdown(); + // return; + // } + // let currentSupportedEditionValue = this._dbSupportedEditionsDropdown.value as any; + // if (!currentSupportedEditionValue.name) { + // this._dbSupportedFamilyDropdown.updateProperties({ + // values: [ + // { + // displayName: localize('deployAzureSQLDB.NoSupportedEditionLabel', "Supported Edition not selected"), + // name: '' + // } + // ] + // }); + // this._dbSupportedFamilyDropdown.loading = false; + // await this.populateVCoreDropdown(); + // return; + // } + + // if (currentSupportedEditionValue.name.length === 0) { + // this._dbSupportedFamilyDropdown.updateProperties({ + // values: [ + // { + // displayName: localize('deployAzureSQLDB.NoSupportedFamiliesLabel', "No database family types found."), + // name: '' + // } + // ], + // }); + // this._dbSupportedFamilyDropdown.loading = false; + // await this.populateVCoreDropdown(); + // return; + // } else { + // currentSupportedEditionValue.name.sort((a: any, b: any) => (a!.name > b!.name) ? 1 : -1); + // } + // this.wizard.addDropdownValues( + // this._dbSupportedFamilyDropdown, + // currentSupportedEditionValue.name.map((value: any) => { + // return { + // displayName: value.name, + // name: value + // }; + // }) + // ); + // if (this._dbSupportedFamilyDropdown.value) { + // this.wizard.model.databaseFamily = (this._dbSupportedFamilyDropdown.value as any).displayName; + // } + // this._dbSupportedFamilyDropdown.loading = false; + // await this.populateVCoreDropdown(); + // return; + // } + + // private async createVCoreDropdown(view: azdata.ModelView) { + // this._dbVCoreDropdown = view.modelBuilder.dropDown().withProperties({ + // required: true, + // }).component(); + // this._dbVCoreDropdown.onValueChanged(async (value) => { + // this.wizard.model.vCoreNumber = value.selected; + // }); + // } + + // private async populateVCoreDropdown() { + // this._dbVCoreDropdown.loading = true; + // if (!this._dbSupportedFamilyDropdown.values || this._dbSupportedFamilyDropdown.values!.length === 0) { + // this._dbVCoreDropdown.updateProperties({ + // values: [] + // }); + // this._dbVCoreDropdown.loading = false; + // return; + // } + // let currentSupportedFamilyValue = this._dbSupportedFamilyDropdown.value as any; + // if (!currentSupportedFamilyValue.name && !currentSupportedFamilyValue.name.supportedVcoresValues) { + // this._dbVCoreDropdown.updateProperties({ + // values: [ + // { + // displayName: localize('deployAzureSQLDB.NoSupportedFamilyLabel', "Supported Family not selected"), + // name: '' + // } + // ] + // }); + // this._dbVCoreDropdown.loading = false; + // return; + // } + + // if (currentSupportedFamilyValue.name.supportedVcoresValues === 0) { + // this._dbVCoreDropdown.updateProperties({ + // values: [ + // { + // displayName: localize('deployAzureSQLDB.NoSupportedVCoreValuesLabel', "No VCore values found."), + // name: '' + // } + // ], + // }); + // this._dbVCoreDropdown.loading = false; + // return; + // } else { + // currentSupportedFamilyValue.name.supportedVcoresValues.sort((a: any, b: any) => (a!.value > b!.value) ? 1 : -1); + // } + + // this.wizard.addDropdownValues( + // this._dbVCoreDropdown, + // currentSupportedFamilyValue.name.supportedVcoresValues.map((value: any) => { + // return { + // displayName: String(value.value), + // name: value.status + // }; + // }) + // ); + // for (let i = 0; i < this._dbVCoreDropdown.values!.length; i++) { + // let value = this._dbVCoreDropdown.values![i] as azdata.CategoryValue; + // if (value.name === 'Default') { + // this._dbVCoreDropdown.value = this._dbVCoreDropdown.values![i]; + // break; + // } + // } + + // if (this._dbVCoreDropdown.value) { + // this.wizard.model.vCoreNumber = Number((this._dbVCoreDropdown.value as any).displayName); + // } + // this._dbVCoreDropdown.loading = false; + // return; + // } + + // private createMaxMemoryText(view: azdata.ModelView) { + // this._dbMemoryTextBox = view.modelBuilder.inputBox().withProperties({ + // inputType: 'number', + // max: 1024, + // min: 1, + // value: '32', + // required: true + // }).component(); + + // this._dbMemoryTextBox.onTextChanged((value) => { + // this.wizard.model.storageInGB = value + 'GB'; + // }); + // } + + private registerValidator(): void { + this.wizard.wizardObject.registerNavigationValidator(async (pcInfo) => { + if (pcInfo.newPage < pcInfo.lastPage) { + return true; + } + let errorMessage = await this.validate(); + + if (errorMessage !== '') { + return false; + } + return true; + }); + } + + protected async validate(): Promise { + let errorMessages = []; + let serverName = (this._serverGroupDropdown.value as azdata.CategoryValue).name; + if (serverName === '') { + errorMessages.push(localize('deployAzureSQLDB.NoServerError', "No servers found in current subscription.\nSelect a different subscription containing at least one server")); + } + // let supportedEditionName = (this._dbSupportedEditionsDropdown.value as azdata.CategoryValue).name; + // if (supportedEditionName === '') { + // errorMessages.push(localize('deployAzureSQLDB.SupportedEditionError', "No Supported DB Edition found in current server.\nSelect a different server")); + // } + // let familyName = (this._dbSupportedFamilyDropdown.value as azdata.CategoryValue).name; + // if (familyName === '') { + // errorMessages.push(localize('deployAzureSQLDB.SupportedFamiliesError', "No Supported Family found in current DB edition.\nSelect a different edition")); + // } + + this.wizard.showErrorMessage(errorMessages.join(EOL)); + return errorMessages.join(EOL); + } +} diff --git a/extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/pages/basePage.ts b/extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/pages/basePage.ts new file mode 100644 index 0000000000..2528167213 --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/pages/basePage.ts @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { DeployAzureSQLDBWizard } from '../deployAzureSQLDBWizard'; + +export abstract class BasePage extends WizardPageBase { + public abstract initialize(): void; +} diff --git a/extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/pages/databaseSettingsPage.ts b/extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/pages/databaseSettingsPage.ts new file mode 100644 index 0000000000..2bd0fc4ab5 --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/pages/databaseSettingsPage.ts @@ -0,0 +1,237 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { DeployAzureSQLDBWizard } from '../deployAzureSQLDBWizard'; +import * as constants from '../constants'; +import { BasePage } from './basePage'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); + +export class DatabaseSettingsPage extends BasePage { + + private _startIpAddressTextRow!: azdata.FlexContainer; + private _startIpAddressTextbox!: azdata.InputBoxComponent; + private _endIpAddressTextRow!: azdata.FlexContainer; + private _endIpAddressTextbox!: azdata.InputBoxComponent; + private _firewallRuleNameTextbox!: azdata.InputBoxComponent; + private _firewallRuleNameTextRow!: azdata.FlexContainer; + private _databaseNameTextbox!: azdata.InputBoxComponent; + private _databaseNameTextRow!: azdata.FlexContainer; + private _collationTextbox!: azdata.InputBoxComponent; + private _collationTextRow!: azdata.FlexContainer; + private _IpInfoText!: azdata.TextComponent; + + private _form!: azdata.FormContainer; + + constructor(wizard: DeployAzureSQLDBWizard) { + super( + constants.DatabaseSettingsPageTitle, + '', + wizard + ); + } + + public async initialize() { + this.pageObject.registerContent(async (view: azdata.ModelView) => { + await Promise.all([ + this.createIpAddressText(view), + this.createFirewallNameText(view), + this.createDatabaseNameText(view), + this.createCollationText(view) + ]); + this._form = view.modelBuilder.formContainer() + .withFormItems( + [ + { + component: this._databaseNameTextRow + }, + { + component: this._collationTextRow + }, + { + component: this._firewallRuleNameTextRow + }, + { + component: this._startIpAddressTextRow + }, + { + component: this._endIpAddressTextRow + }, + { + component: this._IpInfoText + } + ], + { + horizontal: false, + componentWidth: '100%' + }) + .withLayout({ width: '100%' }) + .component(); + + return view.initializeModel(this._form); + }); + } + + public async onEnter(): Promise { + this.wizard.wizardObject.registerNavigationValidator(async (pcInfo) => { + if (pcInfo.newPage < pcInfo.lastPage) { + return true; + } + let errorMessage = await this.validate(); + + if (errorMessage !== '') { + return false; + } + return true; + }); + } + + public async onLeave(): Promise { + this.wizard.wizardObject.registerNavigationValidator((pcInfo) => { + return true; + }); + } + + private createIpAddressText(view: azdata.ModelView) { + + this._IpInfoText = view.modelBuilder.text() + .withProperties({ + value: constants.IpAddressInfoLabel + }).component(); + + //Start IP Address Section: + + this._startIpAddressTextbox = view.modelBuilder.inputBox().withProperties({ + inputType: 'text' + }).component(); + + this._startIpAddressTextbox.onTextChanged((value) => { + this.wizard.model.startIpAddress = value; + }); + + this._startIpAddressTextRow = this.wizard.createFormRowComponent(view, constants.StartIpAddressLabel, '', this._startIpAddressTextbox, true); + + //End IP Address Section: + + this._endIpAddressTextbox = view.modelBuilder.inputBox().withProperties({ + inputType: 'text' + }).component(); + + this._endIpAddressTextbox.onTextChanged((value) => { + this.wizard.model.endIpAddress = value; + }); + + this._endIpAddressTextRow = this.wizard.createFormRowComponent(view, constants.EndIpAddressLabel, '', this._endIpAddressTextbox, true); + } + + private createFirewallNameText(view: azdata.ModelView) { + + this._firewallRuleNameTextbox = view.modelBuilder.inputBox().component(); + + this._firewallRuleNameTextRow = this.wizard.createFormRowComponent(view, constants.FirewallRuleNameLabel, '', this._firewallRuleNameTextbox, true); + + this._firewallRuleNameTextbox.onTextChanged((value) => { + this.wizard.model.firewallRuleName = value; + }); + } + + private createDatabaseNameText(view: azdata.ModelView) { + + this._databaseNameTextbox = view.modelBuilder.inputBox().component(); + + this._databaseNameTextRow = this.wizard.createFormRowComponent(view, constants.DatabaseNameLabel, '', this._databaseNameTextbox, true); + + this._databaseNameTextbox.onTextChanged((value) => { + this.wizard.model.databaseName = value; + }); + } + + private createCollationText(view: azdata.ModelView) { + this._collationTextbox = view.modelBuilder.inputBox().withProperties({ + inputType: 'text', + value: 'SQL_Latin1_General_CP1_CI_AS' + }).component(); + + this._collationTextbox.onTextChanged((value) => { + this.wizard.model.databaseCollation = value; + }); + + this._collationTextRow = this.wizard.createFormRowComponent(view, constants.CollationNameLabel, '', this._collationTextbox, true); + } + + + protected async validate(): Promise { + let errorMessages = []; + let ipRegex = /(^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$)/; + let startipvalue = this._startIpAddressTextbox.value!; + let endipvalue = this._endIpAddressTextbox.value!; + let firewallname = this._firewallRuleNameTextbox.value!; + let databasename = this._databaseNameTextbox.value!; + let collationname = this._collationTextbox.value!; + + if (!(ipRegex.test(startipvalue))) { + errorMessages.push(localize('deployAzureSQLDB.DBMinIpInvalidError', "Min Ip address is invalid")); + } + + if (!(ipRegex.test(endipvalue))) { + errorMessages.push(localize('deployAzureSQLDB.DBMaxIpInvalidError', "Max Ip address is invalid")); + } + + if (/^\d+$/.test(firewallname)) { + errorMessages.push(localize('deployAzureSQLDB.DBFirewallOnlyNumericNameError', "Firewall name cannot contain only numbers.")); + } + if (firewallname.length < 1 || firewallname.length > 100) { + errorMessages.push(localize('deployAzureSQLDB.DBFirewallLengthError', "Firewall name must be between 1 and 100 characters long.")); + } + if (/[\\\/"\'\[\]:\|<>\+=;,\?\*@\&,]/g.test(firewallname)) { + errorMessages.push(localize('deployAzureSQLDB.DBFirewallSpecialCharError', "Firewall name cannot contain special characters \/\"\"[]:|<>+=;,?*@&, .")); + } + if (/[A-Z]/g.test(firewallname)) { + errorMessages.push(localize('deployAzureSQLDB.DBFirewallUpperCaseError', "Upper case letters are not allowed for firealll name")); + } + + if (/^\d+$/.test(databasename)) { + errorMessages.push(localize('deployAzureSQLDB.DBNameOnlyNumericNameError', "Database name cannot contain only numbers.")); + } + if (databasename.length < 1 || databasename.length > 100) { + errorMessages.push(localize('deployAzureSQLDB.DBNameLengthError', "Database name must be between 1 and 100 characters long.")); + } + if (/[\\\/"\'\[\]:\|<>\+=;,\?\*@\&,]/g.test(databasename)) { + errorMessages.push(localize('deployAzureSQLDB.DBNameSpecialCharError', "Database name cannot contain special characters \/\"\"[]:|<>+=;,?*@&, .")); + } + if (await this.databaseNameExists(databasename)) { + errorMessages.push(localize('deployAzureSQLDB.DBNameExistsError', "Database name must be unique in the current server.")); + } + + if (/^\d+$/.test(collationname)) { + errorMessages.push(localize('deployAzureSQLDB.DBCollationOnlyNumericNameError', "Collation name cannot contain only numbers.")); + } + if (collationname.length < 1 || collationname.length > 100) { + errorMessages.push(localize('deployAzureSQLDB.DBCollationLengthError', "Collation name must be between 1 and 100 characters long.")); + } + if (/[\\\/"\'\[\]:\|<>\+=;,\?\*@\&,]/g.test(collationname)) { + errorMessages.push(localize('deployAzureSQLDB.DBCollationSpecialCharError', "Collation name cannot contain special characters \/\"\"[]:|<>+=;,?*@&, .")); + } + + this.wizard.showErrorMessage(errorMessages.join(EOL)); + return errorMessages.join(EOL); + } + + protected async databaseNameExists(dbName: string): Promise { + const url = `https://management.azure.com` + + `/subscriptions/${this.wizard.model.azureSubscription}` + + `/resourceGroups/${this.wizard.model.azureResouceGroup}` + + `/providers/Microsoft.Sql` + + `/servers/${this.wizard.model.azureServerName}` + + `/databases?api-version=2017-10-01-preview`; + + let response = await this.wizard.getRequest(url, true); + + let nameArray = response.data.value.map((v: any) => { return v.name; }); + return (nameArray.includes(dbName)); + } +} diff --git a/extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/pages/summaryPage.ts b/extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/pages/summaryPage.ts new file mode 100644 index 0000000000..87e0725604 --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployAzureSQLDBWizard/pages/summaryPage.ts @@ -0,0 +1,225 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { DeployAzureSQLDBWizard } from '../deployAzureSQLDBWizard'; +import * as constants from '../constants'; +import { SectionInfo, LabelPosition, FontWeight, FieldType } from '../../../interfaces'; +import { createSection } from '../../modelViewUtils'; + +export class AzureSQLDBSummaryPage extends WizardPageBase { + + private formItems: azdata.FormComponent[] = []; + private _form!: azdata.FormBuilder; + private _view!: azdata.ModelView; + + constructor(wizard: DeployAzureSQLDBWizard) { + 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.AzureSettingsSummaryPageTitle, + fields: [ + { + type: FieldType.ReadonlyText, + label: constants.AzureAccountDropdownLabel, + defaultValue: model.azureAccount.displayInfo.displayName, + labelCSSStyles: { fontWeight: FontWeight.Bold } + }, + { + type: FieldType.ReadonlyText, + label: constants.AzureAccountSubscriptionDropdownLabel, + defaultValue: model.azureSubscriptionDisplayName, + labelCSSStyles: { fontWeight: FontWeight.Bold } + }, + { + type: FieldType.ReadonlyText, + label: constants.AzureAccountResourceGroupDropdownLabel, + defaultValue: model.azureResouceGroup, + labelCSSStyles: { fontWeight: FontWeight.Bold } + }, + { + type: FieldType.ReadonlyText, + label: constants.AzureAccountDatabaseServersDropdownLabel, + defaultValue: model.azureServerName, + labelCSSStyles: { fontWeight: FontWeight.Bold } + } + ] + }; + + // const databaseHardwareSettingSection: SectionInfo = { //@todo alma1 9/8/2020 section used for upcoming database hardware creation feature. + // labelPosition: LabelPosition.Left, + // labelWidth: labelWidth, + // inputWidth: inputWidth, + // fieldHeight: fieldHeight, + // spaceBetweenFields: '0', + // title: constants.DatabaseHardwareInfoLabel, + // fields: [ + // { + // type: FieldType.ReadonlyText, + // label: constants.DatabaseSupportedEditionsDropdownLabel, + // defaultValue: model.databaseEdition, + // labelCSSStyles: { fontWeight: FontWeight.Bold } + // }, + // { + // type: FieldType.ReadonlyText, + // label: constants.DatabaseSupportedFamilyDropdownLabel, + // defaultValue: model.databaseFamily, + // labelCSSStyles: { fontWeight: FontWeight.Bold } + // }, + // { + // type: FieldType.ReadonlyText, + // label: constants.DatabaseVCoreNumberDropdownLabel, + // defaultValue: String(model.vCoreNumber), + // labelCSSStyles: { fontWeight: FontWeight.Bold } + // }, + // { + // type: FieldType.ReadonlyText, + // label: constants.DatabaseMaxMemorySummaryTextLabel, + // defaultValue: model.storageInGB, + // labelCSSStyles: { fontWeight: FontWeight.Bold } + // } + // ] + // }; + + const databaseSettingSection: SectionInfo = { + labelPosition: LabelPosition.Left, + labelWidth: labelWidth, + inputWidth: inputWidth, + fieldHeight: fieldHeight, + title: constants.DatabaseSettingsPageTitle, + fields: [ + { + type: FieldType.ReadonlyText, + label: constants.DatabaseNameLabel, + defaultValue: model.databaseName, + labelCSSStyles: { fontWeight: FontWeight.Bold } + }, + { + type: FieldType.ReadonlyText, + label: constants.CollationNameSummaryLabel, + defaultValue: model.databaseCollation, + labelCSSStyles: { fontWeight: FontWeight.Bold } + }, + { + type: FieldType.ReadonlyText, + label: constants.FirewallRuleNameLabel, + defaultValue: model.firewallRuleName, + labelCSSStyles: { fontWeight: FontWeight.Bold } + }, + { + type: FieldType.ReadonlyText, + label: constants.StartIpAddressShortLabel, + defaultValue: model.startIpAddress, + labelCSSStyles: { fontWeight: FontWeight.Bold } + }, + { + type: FieldType.ReadonlyText, + label: constants.EndIpAddressShortLabel, + defaultValue: model.endIpAddress, + 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 databaseHardwareSection = await createSectionFunc(databaseHardwareSettingSection); //@todo alma1 9/8/2020 used for upcoming database hardware creation feature. + const databaseSection = await createSectionFunc(databaseSettingSection); + + this.formItems.push(azureSection, /*databaseHardwareSection,*/ databaseSection); + 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; + } +} 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..2b4a987846 --- /dev/null +++ b/extensions/resource-deployment/src/ui/deployAzureSQLVMWizard/deployAzureSQLVMWizard.ts @@ -0,0 +1,224 @@ +/*--------------------------------------------------------------------------------------------- + * 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.openNotebookWithEdits(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)); + + } + + + +} diff --git a/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts b/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts index 851393ea43..43a65b56fa 100644 --- a/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts +++ b/extensions/resource-deployment/src/ui/resourceTypePickerDialog.ts @@ -273,10 +273,6 @@ export class ResourceTypePickerDialog extends DialogBase { private selectResourceType(resourceType: ResourceType): void { this._currentResourceTypeDisposables.forEach(disposable => disposable.dispose()); this._selectedResourceType = resourceType; - - //handle special case when resource type has different OK button. - this._dialogObject.okButton.label = this._selectedResourceType.okButtonText || loc.select; - this._agreementCheckboxChecked = false; this._agreementContainer.clearItems(); if (resourceType.agreement) { @@ -299,15 +295,26 @@ export class ResourceTypePickerDialog extends DialogBase { ariaLabel: option.displayName }).component(); - this._currentResourceTypeDisposables.push(optionSelectBox.onValueChanged(() => { this.updateToolsDisplayTable(); })); + this._currentResourceTypeDisposables.push(optionSelectBox.onValueChanged(() => { + this.updateOkButtonText(); + this.updateToolsDisplayTable(); + })); + this._optionDropDownMap.set(option.name, optionSelectBox); const row = this._view.modelBuilder.flexContainer().withItems([optionLabel, optionSelectBox], { flex: '0 0 auto', CSSStyles: { 'margin-right': '20px' } }).withLayout({ flexFlow: 'row', alignItems: 'center' }).component(); this._optionsContainer.addItem(row); }); } + this.updateOkButtonText(); this.updateToolsDisplayTable(); } + private updateOkButtonText(): void { + //handle special case when resource type has different OK button. + let text = this.getCurrentOkText(); + this._dialogObject.okButton.label = text || loc.select; + } + private updateToolsDisplayTable(): void { this.toolRefreshTimestamp = new Date().getTime(); const currentRefreshTimestamp = this.toolRefreshTimestamp; @@ -482,6 +489,17 @@ export class ResourceTypePickerDialog extends DialogBase { return this._selectedResourceType.getProvider(options)!; } + private getCurrentOkText(): string { + const options: { option: string, value: string }[] = []; + + this._optionDropDownMap.forEach((selectBox, option) => { + let selectedValue: azdata.CategoryValue = selectBox.value as azdata.CategoryValue; + options.push({ option: option, value: selectedValue.name }); + }); + + return this._selectedResourceType.getOkButtonText(options)!; + } + protected async onComplete(): Promise { this.toolsService.toolsForCurrentProvider = this._tools; this.resourceTypeService.startDeployment(this.getCurrentProvider());