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());