mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
SQL Edge deployment using Azure IoT hub (#11202)
* Azure IoT deployment type * more updates * organize fields * a few more improvements * resolve merge issues * new rg improvement * fix tests * comments 1 * comments 2
This commit is contained in:
@@ -0,0 +1,517 @@
|
|||||||
|
{
|
||||||
|
"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": [
|
||||||
|
"\n",
|
||||||
|
"## Run Azure SQL Database Edge image on a device\n",
|
||||||
|
"This notebook deploy the Azure SQL Database Edge module on a device and connect to it in Azure Data Studio\n",
|
||||||
|
"\n",
|
||||||
|
"### Dependencies\n",
|
||||||
|
"- Azure CLI. For more information, see [Azure CLI Installation](https://docs.microsoft.com/cli/azure/install-azure-cli?view=azure-cli-latest).\n",
|
||||||
|
"\n",
|
||||||
|
"<span style=\"color:red\"><font size=\"3\">Please press the \"Run all\" button to run the notebook</font></span>"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "15b8cfc7-dd7f-4db8-9a3c-2151932fe7b5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"source": [
|
||||||
|
"### Check dependencies"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "f78f4ff3-d4c9-4c3e-853f-4add05061eb0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"import pandas,sys,os,json,html,getpass,time,ntpath,uuid\n",
|
||||||
|
"pandas_version = pandas.__version__.split('.')\n",
|
||||||
|
"pandas_major = int(pandas_version[0])\n",
|
||||||
|
"pandas_minor = int(pandas_version[1])\n",
|
||||||
|
"pandas_patch = int(pandas_version[2])\n",
|
||||||
|
"if not (pandas_major > 0 or (pandas_major == 0 and pandas_minor > 24) or (pandas_major == 0 and pandas_minor == 24 and pandas_patch >= 2)):\n",
|
||||||
|
" sys.exit('Please upgrade the Notebook dependency before you can proceed, you can do it by running the \"Reinstall Notebook dependencies\" command in command palette (View menu -> Command Palette…).')\n",
|
||||||
|
"\n",
|
||||||
|
"def run_command(command:str, displayCommand:str = \"\", returnObject:bool = False):\n",
|
||||||
|
" print(\"Executing: \" + (displayCommand if displayCommand != \"\" else command))\n",
|
||||||
|
" if returnObject:\n",
|
||||||
|
" output = os.popen(command).read()\n",
|
||||||
|
" print(f'Command successfully executed')\n",
|
||||||
|
" return json.loads(''.join(output))\n",
|
||||||
|
" else:\n",
|
||||||
|
" !{command}\n",
|
||||||
|
" if _exit_code != 0:\n",
|
||||||
|
" sys.exit(f'Command execution failed with exit code: {str(_exit_code)}.\\n')\n",
|
||||||
|
" else:\n",
|
||||||
|
" print(f'Command successfully executed')\n",
|
||||||
|
"\n",
|
||||||
|
"run_command(command='az --version')"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "70b9744f-eb59-44e8-9b35-db590ac4651d",
|
||||||
|
"tags": [
|
||||||
|
"hide_input"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"source": [
|
||||||
|
"### Install Azure IoT extension for Azure CLI"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "a7f15c68-1725-4caa-b4f7-ddc2b4934883"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"run_command('az extension add --name azure-cli-iot-ext')"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "55bb2f96-6f7f-4aa0-9daf-d0f7f9d9243c",
|
||||||
|
"tags": [
|
||||||
|
"hide_input"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"source": [
|
||||||
|
"### Required information"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "b5dc5586-06e8-44d9-8bc3-2861d510efe5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"azure_subscription_id = os.environ[\"AZDATA_NB_VAR_ASDE_SUBSCRIPTIONID\"]\n",
|
||||||
|
"azure_resource_group = os.environ[\"AZDATA_NB_VAR_ASDE_RESOURCEGROUP\"]\n",
|
||||||
|
"azure_location = os.environ[\"AZDATA_NB_VAR_ASDE_AZURE_LOCATION_TEXT\"]\n",
|
||||||
|
"sa_password = os.environ[\"AZDATA_NB_VAR_SA_PASSWORD\"]\n",
|
||||||
|
"vm_admin = os.environ[\"AZDATA_NB_VAR_ASDE_VM_ADMIN\"]\n",
|
||||||
|
"vm_password = os.environ[\"AZDATA_NB_VAR_ASDE_VM_PASSWORD\"]\n",
|
||||||
|
"iot_device_id = os.environ[\"AZDATA_NB_VAR_ASDE_DEVICE_ID\"]\n",
|
||||||
|
"dacpac_path = os.environ[\"AZDATA_NB_VAR_ASDE_DACPAC_PATH\"]\n",
|
||||||
|
"sql_port = os.environ[\"AZDATA_NB_VAR_ASDE_SQL_PORT\"]\n",
|
||||||
|
"new_rg_flag = os.environ[\"AZDATA_NB_VAR_ASDE_NEW_RESOURCEGROUP\"]\n",
|
||||||
|
"new_rg_name = os.environ[\"AZDATA_NB_VAR_ASDE_NEW_RESOURCEGROUP_NAME\"]\n",
|
||||||
|
"\n",
|
||||||
|
"if new_rg_flag == 'true':\n",
|
||||||
|
" azure_resource_group = new_rg_name\n",
|
||||||
|
"print(f'Subscription: {azure_subscription_id}')\n",
|
||||||
|
"print(f'Resource group: {azure_resource_group}')\n",
|
||||||
|
"print(f'Location: {azure_location}')\n",
|
||||||
|
"print(f'VM name: {iot_device_id}')\n",
|
||||||
|
"print(f'VM admin username: {vm_admin}')\n",
|
||||||
|
"print(f'VM admin password: ******')\n",
|
||||||
|
"print(f'SQL Server port: {sql_port}')\n",
|
||||||
|
"print(f'SQL Server sa password: ******')\n",
|
||||||
|
"print(f'Dacpac path: {dacpac_path}')"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "dde9388b-f623-4d62-bb74-36a05f5d2ea3",
|
||||||
|
"tags": [
|
||||||
|
"hide_input"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"source": [
|
||||||
|
"### Default settings"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "2a5755eb-85a7-4237-8d87-04cdab13cf40"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"network_security_group = 'asde_nsg'\n",
|
||||||
|
"public_ip_address_name = 'asde_public_ip'\n",
|
||||||
|
"iot_hub_name = 'asdeiothub'\n",
|
||||||
|
"iot_hub_sku = 'S1'\n",
|
||||||
|
"iot_hub_units = 4\n",
|
||||||
|
"vm_size = 'Standard_DS3_v2'\n",
|
||||||
|
"vnet_name = 'asde_network'\n",
|
||||||
|
"subnet_name = 'asde_subnet'\n",
|
||||||
|
"subnet_address_prefix = '10.0.0.0/24'\n",
|
||||||
|
"vnet_address_prefix = '10.0.0.0/16'\n",
|
||||||
|
"azure_storage_account = azure_resource_group.lower()\n",
|
||||||
|
"storage_account_container = 'sqldatabasedacpac'\n",
|
||||||
|
"sql_lcid = '1033'\n",
|
||||||
|
"sql_collation = 'SQL_Latin1_General_CP1_CI_AS'"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "19ebeaf4-94c9-4d2b-bd9f-e3c6bf7f2dda",
|
||||||
|
"tags": [
|
||||||
|
"hide_input"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"source": [
|
||||||
|
"### Login to Azure"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "84f57c09-5772-4f7a-a270-4039b8d5b081"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"run_command('az login')"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "f9e8ddee-aefa-4951-b767-b318d941d2cd",
|
||||||
|
"tags": [
|
||||||
|
"hide_input"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"source": [
|
||||||
|
"### Set active Azure subscription"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "59249fa6-f76c-4e5d-bee7-a9ebef6f873e"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"if azure_subscription_id != \"\":\n",
|
||||||
|
" run_command(f'az account set --subscription {azure_subscription_id}')\n",
|
||||||
|
"else:\n",
|
||||||
|
" print('Using the default Azure subscription', {azure_subscription_id})\n",
|
||||||
|
"run_command(f'az account show')"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "6e085676-2cc5-4af8-819c-fa210244e6c3",
|
||||||
|
"tags": [
|
||||||
|
"hide_input"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"source": [
|
||||||
|
"### Create resource group"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "67dacbaa-92f4-4d06-90bb-8974964852aa"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"rg_exists = run_command(f'az group exists --name {azure_resource_group}', returnObject=True)\n",
|
||||||
|
"\n",
|
||||||
|
"if rg_exists:\n",
|
||||||
|
" print(f'resource group \\\"{azure_resource_group}\\\" already exists.')\n",
|
||||||
|
"else:\n",
|
||||||
|
" run_command(f'az group create --location {azure_location} --name {azure_resource_group}')"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "f29b439e-cf05-4c35-aa47-1482ccd653bf",
|
||||||
|
"tags": [
|
||||||
|
"hide_input"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"source": [
|
||||||
|
"### Create storage account and storage account container, then upload the dacpac"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "90ec2b26-0c4a-4aa4-b397-f16b09b454ea"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"if dacpac_path == \"\":\n",
|
||||||
|
" print(f'Dacpac zip file not provided')\n",
|
||||||
|
" blob_sas = ''\n",
|
||||||
|
"else: \n",
|
||||||
|
" dacpac_name = ntpath.basename(dacpac_path)\n",
|
||||||
|
" storage_accounts = run_command(f'az storage account list --resource-group {azure_resource_group} --subscription {azure_subscription_id}', returnObject=True)\n",
|
||||||
|
" storage_accounts = [storage_account for storage_account in storage_accounts if storage_account['name'] == azure_storage_account]\n",
|
||||||
|
" if len(storage_accounts) == 0:\n",
|
||||||
|
" run_command(f'az storage account create -n {azure_storage_account} -g {azure_resource_group} -l {azure_location} --sku Standard_LRS --kind Storage')\n",
|
||||||
|
" else:\n",
|
||||||
|
" print(f'storage account \\\"{azure_storage_account}\\\" already exists.')\n",
|
||||||
|
"\n",
|
||||||
|
" storage_account_key = run_command(f'az storage account keys list --account-name {azure_storage_account} --resource-group {azure_resource_group}', returnObject=True)[0]['value']\n",
|
||||||
|
" container_exists = run_command(f'az storage container exists --name {storage_account_container} --account-key {storage_account_key} --account-name {azure_storage_account} --auth-mode key --output json', returnObject=True)['exists']\n",
|
||||||
|
" if container_exists:\n",
|
||||||
|
" print(f'storage account container \\\"{storage_account_container}\\\" already exists.')\n",
|
||||||
|
" else:\n",
|
||||||
|
" run_command(f'az storage container create --name {storage_account_container} --account-key {storage_account_key} --account-name {azure_storage_account} --auth-mode key')\n",
|
||||||
|
"\n",
|
||||||
|
" blob_exists = run_command(f'az storage blob exists --container-name {storage_account_container} --name \\\"{dacpac_name}\\\" --account-key {storage_account_key} --account-name {azure_storage_account} --auth-mode key', returnObject=True)['exists']\n",
|
||||||
|
" if blob_exists:\n",
|
||||||
|
" print(f'blob \\\"{dacpac_name}\\\" already exists.')\n",
|
||||||
|
" else:\n",
|
||||||
|
" run_command(f'az storage blob upload --account-name {azure_storage_account} --container-name {storage_account_container} --name {dacpac_name} --file \\\"{dacpac_path}\\\" --account-key {storage_account_key} --auth-mode key')\n",
|
||||||
|
" now = time.localtime()\n",
|
||||||
|
" expiry = f'{(now.tm_year + 1)}-{now.tm_mon}-{now.tm_mday}'\n",
|
||||||
|
" blob_sas = run_command(f'az storage blob generate-sas --container-name {storage_account_container} --name \\\"{dacpac_name}\\\" --account-name {azure_storage_account} --account-key {storage_account_key} --auth-mode key --full-uri --https-only --permissions r --expiry {expiry}', returnObject=True)"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "7ab2b3ec-0832-40b3-98c0-4aa87320e7ce",
|
||||||
|
"tags": [
|
||||||
|
"hide_input"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"source": [
|
||||||
|
"### Create network security group"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "b308771b-138a-40ce-a9d3-1d15094d537b"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"nsg_list = run_command(f'az network nsg list --resource-group {azure_resource_group}', returnObject=True)\n",
|
||||||
|
"nsg_list = [nsg for nsg in nsg_list if nsg['name'] == network_security_group]\n",
|
||||||
|
"if len(nsg_list) == 0:\n",
|
||||||
|
" run_command(f'az network nsg create --name {network_security_group} --resource-group {azure_resource_group} --location {azure_location}')\n",
|
||||||
|
" run_command(f'az network nsg rule create --name \\\"SQL\\\" --nsg-name {network_security_group} --priority 100 --resource-group {azure_resource_group} --access Allow --description \\\"Allow SQL\\\" --destination-address-prefixes \\\"*\\\" --destination-port-ranges {sql_port} --direction Inbound --source-address-prefixes Internet --protocol Tcp')\n",
|
||||||
|
"else:\n",
|
||||||
|
" print(f'Network security group \\\"{network_security_group}\\\" already exists.')"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "99cbb95c-b109-4b2e-909b-ff71a62754fb",
|
||||||
|
"tags": [
|
||||||
|
"hide_input"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"source": [
|
||||||
|
"### Create an Edge enabled VM as an Edge device"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "41b10249-cd40-4053-b1b0-b02f562789f7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"vm_list = run_command(f'az vm list --resource-group {azure_resource_group}', returnObject=True)\n",
|
||||||
|
"vm_list = [vm for vm in vm_list if vm['name'] == iot_device_id]\n",
|
||||||
|
"if len(vm_list) == 0:\n",
|
||||||
|
" vm_image = run_command(f'az vm image list --all --location {azure_location} --offer iot_edge_vm_ubuntu --publisher microsoft_iot_edge --sku ubuntu_1604_edgeruntimeonly', returnObject=True)\n",
|
||||||
|
" image_urn = vm_image[0]['urn']\n",
|
||||||
|
" run_command(f'az vm image accept-terms --urn {image_urn}')\n",
|
||||||
|
" vm_password_placeholder = '<admin_password>'\n",
|
||||||
|
" create_vm_command_template = f'az vm create --name {iot_device_id} --resource-group {azure_resource_group} --admin-username {vm_admin} --admin-password {vm_password_placeholder} --authentication-type password --image {image_urn} --location {azure_location} --nsg {network_security_group} --public-ip-address \\\"{public_ip_address_name}\\\" --public-ip-address-allocation static --public-ip-sku Standard --size {vm_size} --subnet {subnet_name} --subnet-address-prefix \\\"{subnet_address_prefix}\\\" --vnet-name {vnet_name} --vnet-address-prefix \\\"{vnet_address_prefix}\\\"'\n",
|
||||||
|
" run_command(create_vm_command_template.replace(vm_password_placeholder, vm_password), displayCommand=create_vm_command_template.replace(vm_password_placeholder, '******'))\n",
|
||||||
|
"else:\n",
|
||||||
|
" print(f'VM \\\"{iot_device_id}\\\" already exists, skipping the vm creation.')\n",
|
||||||
|
"ip_address = run_command(f'az vm show -d -g {azure_resource_group} -n {iot_device_id} --query publicIps', returnObject=True)"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "157fc38f-cf2a-40c6-9c9e-88f45cc5c62f",
|
||||||
|
"tags": [
|
||||||
|
"hide_input"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"source": [
|
||||||
|
"### Create IoT hub"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "e37a04c3-515d-4cb7-99b2-f8bc6167510e"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"hub_list = run_command(f'az iot hub list --resource-group {azure_resource_group}', returnObject=True)\n",
|
||||||
|
"hub_list = [hub for hub in hub_list if hub['name'] == iot_hub_name]\n",
|
||||||
|
"if len(hub_list) == 0:\n",
|
||||||
|
" run_command(f'az iot hub create --name {iot_hub_name} --resource-group {azure_resource_group} --location {azure_location} --sku {iot_hub_sku} --unit {iot_hub_units}')\n",
|
||||||
|
"else:\n",
|
||||||
|
" print(f'IoT hub \\\"{iot_hub_name}\\\" already exists')"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "f9f5e4ec-82a5-45df-a408-ddb0fb21847c",
|
||||||
|
"tags": [
|
||||||
|
"hide_input"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"source": [
|
||||||
|
"### Add the Edge device to the IoT hub"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "fbc5f4ac-dfe0-4543-ace1-49b796251910"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"device_list = run_command(f'az iot hub device-identity list --edge-enabled true --hub-name {iot_hub_name} --resource-group {azure_resource_group}', returnObject=True)\n",
|
||||||
|
"device_list = [device for device in device_list if device['deviceId'] == iot_device_id]\n",
|
||||||
|
"if len(device_list) == 0:\n",
|
||||||
|
" run_command(f'az iot hub device-identity create --device-id {iot_device_id} --hub-name {iot_hub_name} --resource-group {azure_resource_group} --edge-enabled true')\n",
|
||||||
|
"else:\n",
|
||||||
|
" print(f'Edge device \\\"{iot_device_id}\\\" already exists.')"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "c183c3e3-8699-4f29-993b-07bf848336e3",
|
||||||
|
"tags": [
|
||||||
|
"hide_input"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"source": [
|
||||||
|
"### Configure Edge on the device"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "069db017-9169-499a-839b-9cd73ea7d01e"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"connection_string = run_command(f'az iot hub device-identity show-connection-string --device-id {iot_device_id} --hub-name {iot_hub_name} --resource-group {azure_resource_group}', returnObject=True)\n",
|
||||||
|
"connection_string = connection_string['connectionString']\n",
|
||||||
|
"script = f'/etc/iotedge/configedge.sh \\'{connection_string}\\''\n",
|
||||||
|
"run_command(f'az vm run-command invoke -g {azure_resource_group} -n {iot_device_id} --command-id RunShellScript --script \\\"{script}\\\"')"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "9ec1e31a-79aa-49f4-a0e5-16f8d7c2dd21",
|
||||||
|
"tags": [
|
||||||
|
"hide_input"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"source": [
|
||||||
|
"### Deploy Azure SQL Database Edge to the device"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "ec46957f-0795-4c75-804d-f8a7ecb26382"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"manifest = '{\\\"modulesContent\\\":{\\\"$edgeAgent\\\":{\\\"properties.desired\\\":{\\\"modules\\\":{\\\"AzureSQLEdge\\\":{\\\"settings\\\":{\\\"image\\\":\\\"mcr.microsoft.com/azure-sql-edge-developer\\\",\\\"createOptions\\\":\\\"{\\\\\\\"HostConfig\\\\\\\":{\\\\\\\"CapAdd\\\\\\\":[\\\\\\\"SYS_PTRACE\\\\\\\"],\\\\\\\"Binds\\\\\\\":[\\\\\\\"sqlvolume:/sqlvolume\\\\\\\"],\\\\\\\"PortBindings\\\\\\\":{\\\\\\\"1433/tcp\\\\\\\":[{\\\\\\\"HostPort\\\\\\\":\\\\\\\"<SQL_Port>\\\\\\\"}]},\\\\\\\"Mounts\\\\\\\":[{\\\\\\\"Type\\\\\\\":\\\\\\\"volume\\\\\\\",\\\\\\\"Source\\\\\\\":\\\\\\\"sqlvolume\\\\\\\",\\\\\\\"Target\\\\\\\":\\\\\\\"/var/opt/mssql\\\\\\\"}]},\\\\\\\"User\\\\\\\":\\\\\\\"0:0\\\\\\\",\\\\\\\"Env\\\\\\\":[\\\\\\\"MSSQL_AGENT_ENABLED=TRUE\\\\\\\",\\\\\\\"ClientTransportType=AMQP_TCP_Only\\\\\\\",\\\\\\\"MSSQL_PID=Developer\\\\\\\"]}\\\"},\\\"type\\\":\\\"docker\\\",\\\"version\\\":\\\"1.0\\\",\\\"env\\\":{\\\"ACCEPT_EULA\\\":{\\\"value\\\":\\\"Y\\\"},\\\"SA_PASSWORD\\\":{\\\"value\\\":\\\"<Default_SQL_SA_Password>\\\"},\\\"MSSQL_LCID\\\":{\\\"value\\\":\\\"<SQL_LCID>\\\"},\\\"MSSQL_COLLATION\\\":{\\\"value\\\":\\\"<SQL_Collation>\\\"}},\\\"status\\\":\\\"running\\\",\\\"restartPolicy\\\":\\\"always\\\"}},\\\"runtime\\\":{\\\"settings\\\":{\\\"minDockerVersion\\\":\\\"v1.25\\\"},\\\"type\\\":\\\"docker\\\"},\\\"schemaVersion\\\":\\\"1.0\\\",\\\"systemModules\\\":{\\\"edgeAgent\\\":{\\\"settings\\\":{\\\"image\\\":\\\"mcr.microsoft.com/azureiotedge-agent:1.0\\\",\\\"createOptions\\\":\\\"\\\"},\\\"type\\\":\\\"docker\\\"},\\\"edgeHub\\\":{\\\"settings\\\":{\\\"image\\\":\\\"mcr.microsoft.com/azureiotedge-hub:1.0\\\",\\\"createOptions\\\":\\\"{\\\\\\\"HostConfig\\\\\\\":{\\\\\\\"PortBindings\\\\\\\":{\\\\\\\"443/tcp\\\\\\\":[{\\\\\\\"HostPort\\\\\\\":\\\\\\\"443\\\\\\\"}],\\\\\\\"5671/tcp\\\\\\\":[{\\\\\\\"HostPort\\\\\\\":\\\\\\\"5671\\\\\\\"}],\\\\\\\"8883/tcp\\\\\\\":[{\\\\\\\"HostPort\\\\\\\":\\\\\\\"8883\\\\\\\"}]}}}\\\"},\\\"type\\\":\\\"docker\\\",\\\"status\\\":\\\"running\\\",\\\"restartPolicy\\\":\\\"always\\\"}}}},\\\"$edgeHub\\\":{\\\"properties.desired\\\":{\\\"routes\\\":{},\\\"schemaVersion\\\":\\\"1.0\\\",\\\"storeAndForwardConfiguration\\\":{\\\"timeToLiveSecs\\\":7200}}},\\\"AzureSQLEdge\\\":{\\\"properties.desired\\\":{\\\"SqlPackage\\\":\\\"<Optional_DACPAC_ZIP_SAS_URL>\\\",\\\"ASAJobInfo\\\":\\\"<Optional_ASA_Job_ZIP_SAS_URL>\\\"}}}}'\n",
|
||||||
|
"manifest = manifest.replace('<Optional_DACPAC_ZIP_SAS_URL>', blob_sas).replace('<Default_SQL_SA_Password>',sa_password).replace('<SQL_LCID>',sql_lcid).replace('<SQL_Port>',sql_port).replace('<SQL_Collation>',sql_collation)\n",
|
||||||
|
"file_name = f'{uuid.uuid4().hex}.json'\n",
|
||||||
|
"manifest_file = open(file_name, 'w')\n",
|
||||||
|
"manifest_file.write(manifest)\n",
|
||||||
|
"manifest_file.close()\n",
|
||||||
|
"run_command(f'az iot edge set-modules --device-id \\\"{iot_device_id}\\\" --hub-name \\\"{iot_hub_name}\\\" --content \\\"{file_name}\\\" --resource-group {azure_resource_group}')\n",
|
||||||
|
"os.remove(file_name)"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "81a86ff6-5a83-48be-8be7-654d152eea89",
|
||||||
|
"tags": [
|
||||||
|
"hide_input"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"source": [
|
||||||
|
"### **Connect to SQL Server instance in Azure Data Studio**\n",
|
||||||
|
"Click the link below to connect to the SQL Server, it might take a couple minutes for the service to start."
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "3bdfa537-a749-45c4-b219-57d296c22739"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"from IPython.display import *\n",
|
||||||
|
"connectionParameter = '{\"serverName\":\"' + f'{ip_address},{sql_port}' + '\",\"providerName\":\"MSSQL\",\"authenticationType\":\"SqlLogin\",\"userName\": \"sa\",\"password\":' + json.dumps(sa_password) + '}'\n",
|
||||||
|
"display(HTML('<br/><a href=\"command:azdata.connect?' + html.escape(connectionParameter)+'\"><font size=\"3\">Click here to connect to SQL Server</font></a><br/>'))\n",
|
||||||
|
"display(HTML('<br/><span style=\"color:red\"><font size=\"2\">NOTE: The SQL Server password is included in this link, you may want to clear the results of this code cell before saving the notebook.</font></span>'))"
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"azdata_cell_guid": "8bc29cce-96a7-4a78-89af-5c73a6431c24",
|
||||||
|
"tags": [
|
||||||
|
"hide_input"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -41,6 +41,10 @@
|
|||||||
{
|
{
|
||||||
"name": "remote",
|
"name": "remote",
|
||||||
"displayName": "%sql-edge-remote-display-name%"
|
"displayName": "%sql-edge-remote-display-name%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "azure",
|
||||||
|
"displayName": "%sql-edge-azure-display-name%"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -48,7 +52,7 @@
|
|||||||
"providers": [
|
"providers": [
|
||||||
{
|
{
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"notebook": "%sql-edge-local-notebook%",
|
"notebook": "./notebooks/edge/deploy-sql-edge-local.ipynb",
|
||||||
"title": "%sql-edge-local-title%",
|
"title": "%sql-edge-local-title%",
|
||||||
"name": "sql-edge-local-dialog",
|
"name": "sql-edge-local-dialog",
|
||||||
"tabs": [
|
"tabs": [
|
||||||
@@ -141,7 +145,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"dialog": {
|
"dialog": {
|
||||||
"notebook": "%sql-edge-remote-notebook%",
|
"notebook": "./notebooks/edge/deploy-sql-edge-remote.ipynb",
|
||||||
"title": "%sql-edge-remote-title%",
|
"title": "%sql-edge-remote-title%",
|
||||||
"name": "sql-edge-remote-dialog",
|
"name": "sql-edge-remote-dialog",
|
||||||
"tabs": [
|
"tabs": [
|
||||||
@@ -255,6 +259,132 @@
|
|||||||
},
|
},
|
||||||
"requiredTools": [],
|
"requiredTools": [],
|
||||||
"when": "type=remote"
|
"when": "type=remote"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dialog": {
|
||||||
|
"notebook": "./notebooks/edge/deploy-sql-edge-azure.ipynb",
|
||||||
|
"title": "%sql-edge-azure-title%",
|
||||||
|
"name": "sql-edge-azure-dialog",
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"title": "",
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"title": "%azure-info-section-title%",
|
||||||
|
"collapsible": true,
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"subscriptionVariableName": "AZDATA_NB_VAR_ASDE_SUBSCRIPTIONID",
|
||||||
|
"resourceGroupVariableName": "AZDATA_NB_VAR_ASDE_RESOURCEGROUP",
|
||||||
|
"type": "azure_account",
|
||||||
|
"required": true,
|
||||||
|
"allowNewResourceGroup": true,
|
||||||
|
"newResourceGroupFlagVariableName": "AZDATA_NB_VAR_ASDE_NEW_RESOURCEGROUP",
|
||||||
|
"newResourceGroupNameVariableName": "AZDATA_NB_VAR_ASDE_NEW_RESOURCEGROUP_NAME"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "azure_locations",
|
||||||
|
"label": "%azure_location%",
|
||||||
|
"defaultValue": "westus",
|
||||||
|
"required": true,
|
||||||
|
"locationVariableName": "AZDATA_NB_VAR_ASDE_AZURE_LOCATION",
|
||||||
|
"displayLocationVariableName": "AZDATA_NB_VAR_ASDE_AZURE_LOCATION_TEXT",
|
||||||
|
"locations": [
|
||||||
|
"australiaeast",
|
||||||
|
"australiasoutheast",
|
||||||
|
"brazilsouth",
|
||||||
|
"canadacentral",
|
||||||
|
"canadaeast",
|
||||||
|
"centralindia",
|
||||||
|
"centralus",
|
||||||
|
"eastasia",
|
||||||
|
"eastus",
|
||||||
|
"eastus2",
|
||||||
|
"francecentral",
|
||||||
|
"japaneast",
|
||||||
|
"japanwest",
|
||||||
|
"koreacentral",
|
||||||
|
"koreasouth",
|
||||||
|
"northcentralus",
|
||||||
|
"northeurope",
|
||||||
|
"southcentralus",
|
||||||
|
"southindia",
|
||||||
|
"southeastasia",
|
||||||
|
"uksouth",
|
||||||
|
"ukwest",
|
||||||
|
"westcentralus",
|
||||||
|
"westeurope",
|
||||||
|
"westus",
|
||||||
|
"westus2"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "%iot_device_id%",
|
||||||
|
"variableName": "AZDATA_NB_VAR_ASDE_DEVICE_ID",
|
||||||
|
"type": "text",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "%vm_admin%",
|
||||||
|
"variableName": "AZDATA_NB_VAR_ASDE_VM_ADMIN",
|
||||||
|
"type": "text",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "%vm_password%",
|
||||||
|
"variableName": "AZDATA_NB_VAR_ASDE_VM_PASSWORD",
|
||||||
|
"type": "password",
|
||||||
|
"confirmationRequired": true,
|
||||||
|
"confirmationLabel": "%vm_password_confirm%",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "%sqlserver-info-section-title%",
|
||||||
|
"collapsible": true,
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"label": "%docker-sql-password-field%",
|
||||||
|
"variableName": "AZDATA_NB_VAR_SA_PASSWORD",
|
||||||
|
"type": "sql_password",
|
||||||
|
"userName": "sa",
|
||||||
|
"confirmationRequired": true,
|
||||||
|
"confirmationLabel": "%docker-confirm-sql-password-field%",
|
||||||
|
"defaultValue": "",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "%docker-sql-port-field%",
|
||||||
|
"variableName": "AZDATA_NB_VAR_ASDE_SQL_PORT",
|
||||||
|
"type": "number",
|
||||||
|
"defaultValue": 31433,
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "%dacpac_path%",
|
||||||
|
"variableName": "AZDATA_NB_VAR_ASDE_DACPAC_PATH",
|
||||||
|
"type": "file_picker",
|
||||||
|
"required": false,
|
||||||
|
"filter": {
|
||||||
|
"displayName": "%dacpac-zip-files%",
|
||||||
|
"fileTypes": [
|
||||||
|
"zip"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"requiredTools": [
|
||||||
|
{
|
||||||
|
"name": "azure-cli"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"when": "type=azure"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"agreement": {
|
"agreement": {
|
||||||
|
|||||||
@@ -4,14 +4,13 @@
|
|||||||
"docker-container-name-field": "Container name",
|
"docker-container-name-field": "Container name",
|
||||||
"docker-sql-password-field": "SQL Server password",
|
"docker-sql-password-field": "SQL Server password",
|
||||||
"docker-confirm-sql-password-field": "Confirm password",
|
"docker-confirm-sql-password-field": "Confirm password",
|
||||||
"docker-sql-port-field": "Port",
|
"docker-sql-port-field": "SQL Server Port",
|
||||||
"microsoft-privacy-statement": "Microsoft Privacy Statement",
|
"microsoft-privacy-statement": "Microsoft Privacy Statement",
|
||||||
"resource-type-sql-edge-display-name": "Azure SQL DB Edge",
|
"resource-type-sql-edge-display-name": "Azure SQL DB Edge",
|
||||||
"resource-type-sql-edge-description": "Deploy Azure SQL DB Edge (Preview)",
|
"resource-type-sql-edge-description": "Deploy Azure SQL DB Edge (Preview)",
|
||||||
"sql-edge-type-display-name": "Type",
|
"sql-edge-type-display-name": "Deployment target",
|
||||||
"sql-edge-local-display-name": "Local",
|
"sql-edge-local-display-name": "Local",
|
||||||
"sql-edge-remote-display-name": "Remote",
|
"sql-edge-remote-display-name": "Remote",
|
||||||
"sql-edge-local-notebook": "./notebooks/edge/deploy-sql-edge-local.ipynb",
|
|
||||||
"sql-edge-local-title": "Deploy Azure SQL DB Edge locally",
|
"sql-edge-local-title": "Deploy Azure SQL DB Edge locally",
|
||||||
"docker-settings-section-title": "Docker settings",
|
"docker-settings-section-title": "Docker settings",
|
||||||
"docker-registry-field": "Registry",
|
"docker-registry-field": "Registry",
|
||||||
@@ -21,10 +20,22 @@
|
|||||||
"docker-password-field": "Password",
|
"docker-password-field": "Password",
|
||||||
"edge-agreement": "I accept {0} and {1}.",
|
"edge-agreement": "I accept {0} and {1}.",
|
||||||
"edge-eula": "Microsoft SQL Server License Agreement",
|
"edge-eula": "Microsoft SQL Server License Agreement",
|
||||||
"sql-edge-remote-notebook": "./notebooks/edge/deploy-sql-edge-remote.ipynb",
|
|
||||||
"sql-edge-remote-title": "Deploy Azure SQL DB Edge on a remote machine",
|
"sql-edge-remote-title": "Deploy Azure SQL DB Edge on a remote machine",
|
||||||
"remote-info-section-title": "Target machine information",
|
"remote-info-section-title": "Target machine information",
|
||||||
"edge-remote-target-field": "Name or IP address",
|
"edge-remote-target-field": "Name or IP address",
|
||||||
"edge-remote-username-field": "Username",
|
"edge-remote-username-field": "Username",
|
||||||
"edge-remote-password-field": "Password"
|
"edge-remote-password-field": "Password",
|
||||||
|
"sql-edge-azure-display-name": "An Azure VM via Azure IoT Hub",
|
||||||
|
"sql-edge-azure-title": "Deploy Azure SQL DB Edge to an Azure VM via IoT hub",
|
||||||
|
"azure_subscription_id": "Subscription id",
|
||||||
|
"azure_resource_group": "Resource group",
|
||||||
|
"azure_location": "Location",
|
||||||
|
"vm_admin": "VM admin username",
|
||||||
|
"vm_password": "VM admin password",
|
||||||
|
"vm_password_confirm": "Confirm VM admin password",
|
||||||
|
"iot_device_id": "VM name (device id)",
|
||||||
|
"dacpac_path": "Dacpac zip file",
|
||||||
|
"azure-info-section-title": "Azure information",
|
||||||
|
"sqlserver-info-section-title": "SQL Server information",
|
||||||
|
"dacpac-zip-files": "Dacpac zip files"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,6 +236,9 @@ export interface AzureAccountFieldInfo extends AzureLocationsFieldInfo {
|
|||||||
displaySubscriptionVariableName?: string;
|
displaySubscriptionVariableName?: string;
|
||||||
subscriptionVariableName?: string;
|
subscriptionVariableName?: string;
|
||||||
resourceGroupVariableName?: string;
|
resourceGroupVariableName?: string;
|
||||||
|
allowNewResourceGroup?: boolean;
|
||||||
|
newResourceGroupFlagVariableName?: string;
|
||||||
|
newResourceGroupNameVariableName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AzureLocationsFieldInfo extends FieldInfo {
|
export interface AzureLocationsFieldInfo extends FieldInfo {
|
||||||
@@ -244,6 +247,15 @@ export interface AzureLocationsFieldInfo extends FieldInfo {
|
|||||||
locations?: string[]
|
locations?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FilePickerFieldInfo extends FieldInfo {
|
||||||
|
filter: FilePickerFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilePickerFilter {
|
||||||
|
displayName: string;
|
||||||
|
fileTypes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export const enum LabelPosition {
|
export const enum LabelPosition {
|
||||||
Top = 'top',
|
Top = 'top',
|
||||||
Left = 'left'
|
Left = 'left'
|
||||||
|
|||||||
@@ -15,3 +15,9 @@ export const browse = localize('filePicker.browse', "Browse");
|
|||||||
export const select = localize('filePicker.select', "Select");
|
export const select = localize('filePicker.select', "Select");
|
||||||
export const kubeConfigFilePath = localize('kubeConfigClusterPicker.kubeConfigFilePatht', "Kube config file path");
|
export const kubeConfigFilePath = localize('kubeConfigClusterPicker.kubeConfigFilePatht', "Kube config file path");
|
||||||
export const clusterContextNotFound = localize('kubeConfigClusterPicker.clusterContextNotFound', "No cluster context information found");
|
export const clusterContextNotFound = localize('kubeConfigClusterPicker.clusterContextNotFound', "No cluster context information found");
|
||||||
|
export const signIn = localize('azure.signin', "Sign in…");
|
||||||
|
export const refresh = localize('azure.refresh', "Refresh");
|
||||||
|
export const createNewResourceGroup = localize('azure.resourceGroup.createNewResourceGroup', "Create a new resource group");
|
||||||
|
export const NewResourceGroupAriaLabel = localize('azure.resourceGroup.NewResourceGroupAriaLabel', "New resource group name");
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import * as path from 'path';
|
|||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import * as nls from 'vscode-nls';
|
import * as nls from 'vscode-nls';
|
||||||
import { azureResource } from '../../../azurecore/src/azureResource/azure-resource';
|
import { azureResource } from '../../../azurecore/src/azureResource/azure-resource';
|
||||||
import { AzureAccountFieldInfo, AzureLocationsFieldInfo, ComponentCSSStyles, DialogInfoBase, FieldInfo, FieldType, KubeClusterContextFieldInfo, LabelPosition, NoteBookEnvironmentVariablePrefix, OptionsInfo, OptionsType, PageInfoBase, RowInfo, SectionInfo, TextCSSStyles } from '../interfaces';
|
import { AzureAccountFieldInfo, AzureLocationsFieldInfo, ComponentCSSStyles, DialogInfoBase, FieldInfo, FieldType, KubeClusterContextFieldInfo, LabelPosition, NoteBookEnvironmentVariablePrefix, OptionsInfo, OptionsType, PageInfoBase, RowInfo, SectionInfo, TextCSSStyles, FilePickerFieldInfo } from '../interfaces';
|
||||||
import * as loc from '../localizedConstants';
|
import * as loc from '../localizedConstants';
|
||||||
import { getDefaultKubeConfigPath, getKubeConfigClusterContexts } from '../services/kubeService';
|
import { getDefaultKubeConfigPath, getKubeConfigClusterContexts } from '../services/kubeService';
|
||||||
import { assert, getDateTimeString, getErrorMessage } from '../utils';
|
import { assert, getDateTimeString, getErrorMessage } from '../utils';
|
||||||
@@ -98,6 +98,12 @@ interface AzureAccountFieldContext extends FieldContext {
|
|||||||
fieldInfo: AzureAccountFieldInfo;
|
fieldInfo: AzureAccountFieldInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AzureAccountComponents {
|
||||||
|
accountDropdown: azdata.DropDownComponent;
|
||||||
|
signInButton: azdata.ButtonComponent;
|
||||||
|
refreshAccountsButton: azdata.ButtonComponent;
|
||||||
|
}
|
||||||
|
|
||||||
interface ContextBase {
|
interface ContextBase {
|
||||||
container: azdata.window.Dialog | azdata.window.Wizard;
|
container: azdata.window.Dialog | azdata.window.Wizard;
|
||||||
inputComponents: InputComponents;
|
inputComponents: InputComponents;
|
||||||
@@ -602,18 +608,32 @@ function processCheckboxField(context: FieldContext): void {
|
|||||||
* @param context The context to use to create the field
|
* @param context The context to use to create the field
|
||||||
*/
|
*/
|
||||||
function processFilePickerField(context: FieldContext): FilePickerInputs {
|
function processFilePickerField(context: FieldContext): FilePickerInputs {
|
||||||
|
const inputWidth = parseInt(context.fieldInfo.inputWidth!);
|
||||||
|
if (inputWidth === NaN) {
|
||||||
|
// this is a dev time only error
|
||||||
|
throw new Error('Unable to parse the input width of the file picker field');
|
||||||
|
}
|
||||||
|
const buttonWidth = 100;
|
||||||
|
|
||||||
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles });
|
const label = createLabel(context.view, { text: context.fieldInfo.label, description: context.fieldInfo.description, required: context.fieldInfo.required, width: context.fieldInfo.labelWidth, cssStyles: context.fieldInfo.labelCSSStyles });
|
||||||
const input = createTextInput(context.view, {
|
const input = createTextInput(context.view, {
|
||||||
defaultValue: context.fieldInfo.defaultValue || '',
|
defaultValue: context.fieldInfo.defaultValue || '',
|
||||||
ariaLabel: context.fieldInfo.label,
|
ariaLabel: context.fieldInfo.label,
|
||||||
required: context.fieldInfo.required,
|
required: context.fieldInfo.required,
|
||||||
placeHolder: context.fieldInfo.placeHolder,
|
placeHolder: context.fieldInfo.placeHolder,
|
||||||
width: context.fieldInfo.inputWidth,
|
width: `${inputWidth - buttonWidth}px`,
|
||||||
enabled: context.fieldInfo.enabled
|
enabled: context.fieldInfo.enabled
|
||||||
});
|
});
|
||||||
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: input });
|
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: input });
|
||||||
input.enabled = false;
|
input.enabled = false;
|
||||||
const browseFileButton = context.view!.modelBuilder.button().withProperties({ label: loc.browse }).component();
|
const browseFileButton = context.view!.modelBuilder.button().withProperties<azdata.ButtonProperties>({ label: loc.browse, width: buttonWidth }).component();
|
||||||
|
const fieldInfo = context.fieldInfo as FilePickerFieldInfo;
|
||||||
|
let filter: { [name: string]: string[] } | undefined = undefined;
|
||||||
|
if (fieldInfo.filter) {
|
||||||
|
const filterName = fieldInfo.filter.displayName;
|
||||||
|
filter = {};
|
||||||
|
filter[filterName] = fieldInfo.filter.fileTypes;
|
||||||
|
}
|
||||||
context.onNewDisposableCreated(browseFileButton.onDidClick(async () => {
|
context.onNewDisposableCreated(browseFileButton.onDidClick(async () => {
|
||||||
let fileUris = await vscode.window.showOpenDialog({
|
let fileUris = await vscode.window.showOpenDialog({
|
||||||
canSelectFiles: true,
|
canSelectFiles: true,
|
||||||
@@ -621,9 +641,7 @@ function processFilePickerField(context: FieldContext): FilePickerInputs {
|
|||||||
canSelectMany: false,
|
canSelectMany: false,
|
||||||
defaultUri: vscode.Uri.file(path.dirname(input.value || os_homedir())),
|
defaultUri: vscode.Uri.file(path.dirname(input.value || os_homedir())),
|
||||||
openLabel: loc.select,
|
openLabel: loc.select,
|
||||||
filters: {
|
filters: filter
|
||||||
'Config Files': ['*'],
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
if (!fileUris || fileUris.length === 0) {
|
if (!fileUris || fileUris.length === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -631,8 +649,8 @@ function processFilePickerField(context: FieldContext): FilePickerInputs {
|
|||||||
let fileUri = fileUris[0];
|
let fileUri = fileUris[0];
|
||||||
input.value = fileUri.fsPath;
|
input.value = fileUri.fsPath;
|
||||||
}));
|
}));
|
||||||
context.fieldInfo.labelPosition = LabelPosition.Left;
|
const component = createFlexContainer(context.view, [input, browseFileButton], true, context.fieldInfo.inputWidth);
|
||||||
addLabelInputPairToContainer(context.view, context.components, label, input, context.fieldInfo, [browseFileButton]);
|
addLabelInputPairToContainer(context.view, context.components, label, component, context.fieldInfo);
|
||||||
return { input: input, browseButton: browseFileButton };
|
return { input: input, browseButton: browseFileButton };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -684,6 +702,7 @@ async function processKubeConfigClusterPickerField(context: KubeClusterContextFi
|
|||||||
inputWidth: context.fieldInfo.inputWidth,
|
inputWidth: context.fieldInfo.inputWidth,
|
||||||
labelWidth: context.fieldInfo.labelWidth,
|
labelWidth: context.fieldInfo.labelWidth,
|
||||||
variableName: kubeConfigFilePathVariableName,
|
variableName: kubeConfigFilePathVariableName,
|
||||||
|
labelPosition: LabelPosition.Left,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -731,31 +750,66 @@ async function processAzureAccountField(context: AzureAccountFieldContext): Prom
|
|||||||
context.fieldInfo.subFields = [];
|
context.fieldInfo.subFields = [];
|
||||||
const accountValueToAccountMap = new Map<string, azdata.Account>();
|
const accountValueToAccountMap = new Map<string, azdata.Account>();
|
||||||
const subscriptionValueToSubscriptionMap = new Map<string, azureResource.AzureResourceSubscription>();
|
const subscriptionValueToSubscriptionMap = new Map<string, azureResource.AzureResourceSubscription>();
|
||||||
const accountDropdown = createAzureAccountDropdown(context);
|
const accountComponents = createAzureAccountDropdown(context);
|
||||||
|
const accountDropdown = accountComponents.accountDropdown;
|
||||||
const subscriptionDropdown = createAzureSubscriptionDropdown(context, subscriptionValueToSubscriptionMap);
|
const subscriptionDropdown = createAzureSubscriptionDropdown(context, subscriptionValueToSubscriptionMap);
|
||||||
const resourceGroupDropdown = createAzureResourceGroupsDropdown(context, accountDropdown, accountValueToAccountMap, subscriptionDropdown, subscriptionValueToSubscriptionMap);
|
const resourceGroupDropdown = createAzureResourceGroupsDropdown(context, accountDropdown, accountValueToAccountMap, subscriptionDropdown, subscriptionValueToSubscriptionMap);
|
||||||
|
if (context.fieldInfo.allowNewResourceGroup) {
|
||||||
|
const newRGCheckbox = createCheckbox(context.view, { initialValue: false, label: loc.createNewResourceGroup });
|
||||||
|
context.onNewInputComponentCreated(context.fieldInfo.newResourceGroupFlagVariableName!, { component: newRGCheckbox });
|
||||||
|
const newRGNameInput = createTextInput(context.view, { ariaLabel: loc.NewResourceGroupAriaLabel });
|
||||||
|
context.onNewInputComponentCreated(context.fieldInfo.newResourceGroupNameVariableName!, { component: newRGNameInput });
|
||||||
|
context.components.push(newRGCheckbox);
|
||||||
|
context.components.push(newRGNameInput);
|
||||||
|
const setRGStatus = (newRG: boolean) => {
|
||||||
|
resourceGroupDropdown.required = !newRG;
|
||||||
|
resourceGroupDropdown.enabled = !newRG;
|
||||||
|
newRGNameInput.required = newRG;
|
||||||
|
newRGNameInput.enabled = newRG;
|
||||||
|
if (!newRG) {
|
||||||
|
newRGNameInput.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
context.onNewDisposableCreated(newRGCheckbox.onChanged(() => {
|
||||||
|
setRGStatus(newRGCheckbox.checked!);
|
||||||
|
}));
|
||||||
|
setRGStatus(false);
|
||||||
|
}
|
||||||
const locationDropdown = context.fieldInfo.locations && await processAzureLocationsField(context);
|
const locationDropdown = context.fieldInfo.locations && await processAzureLocationsField(context);
|
||||||
accountDropdown.onValueChanged(async selectedItem => {
|
accountDropdown.onValueChanged(async selectedItem => {
|
||||||
const selectedAccount = accountValueToAccountMap.get(selectedItem.selected)!;
|
const selectedAccount = accountValueToAccountMap.get(selectedItem.selected)!;
|
||||||
await handleSelectedAccountChanged(context, selectedAccount, subscriptionDropdown, subscriptionValueToSubscriptionMap, resourceGroupDropdown, locationDropdown);
|
await handleSelectedAccountChanged(context, selectedAccount, subscriptionDropdown, subscriptionValueToSubscriptionMap, resourceGroupDropdown, locationDropdown);
|
||||||
});
|
});
|
||||||
try {
|
|
||||||
const accounts = await azdata.accounts.getAllAccounts();
|
const populateAzureAccounts = async () => {
|
||||||
// Append a blank value for the "default" option if the field isn't required, context will clear all the dropdowns when selected
|
accountValueToAccountMap.clear();
|
||||||
const dropdownValues = context.fieldInfo.required ? [] : [''];
|
try {
|
||||||
accountDropdown.values = dropdownValues.concat(accounts.map(account => {
|
const accounts = await azdata.accounts.getAllAccounts();
|
||||||
const displayName = `${account.displayInfo.displayName} (${account.displayInfo.userId})`;
|
// Append a blank value for the "default" option if the field isn't required, context will clear all the dropdowns when selected
|
||||||
accountValueToAccountMap.set(displayName, account);
|
const dropdownValues = context.fieldInfo.required ? [] : [''];
|
||||||
return displayName;
|
accountDropdown.values = dropdownValues.concat(accounts.map(account => {
|
||||||
}));
|
const displayName = `${account.displayInfo.displayName} (${account.displayInfo.userId})`;
|
||||||
const selectedAccount = accountDropdown.value ? accountValueToAccountMap.get(accountDropdown.value.toString()) : undefined;
|
accountValueToAccountMap.set(displayName, account);
|
||||||
await handleSelectedAccountChanged(context, selectedAccount, subscriptionDropdown, subscriptionValueToSubscriptionMap, resourceGroupDropdown, locationDropdown);
|
return displayName;
|
||||||
} catch (error) {
|
}));
|
||||||
vscode.window.showErrorMessage(localize('azure.accounts.unexpectedAccountsError', 'Unexpected error fetching accounts: ${0}', getErrorMessage(error)));
|
const selectedAccount = accountDropdown.value ? accountValueToAccountMap.get(accountDropdown.value.toString()) : undefined;
|
||||||
}
|
await handleSelectedAccountChanged(context, selectedAccount, subscriptionDropdown, subscriptionValueToSubscriptionMap, resourceGroupDropdown, locationDropdown);
|
||||||
|
} catch (error) {
|
||||||
|
vscode.window.showErrorMessage(localize('azure.accounts.unexpectedAccountsError', 'Unexpected error fetching accounts: ${0}', getErrorMessage(error)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
context.onNewDisposableCreated(accountComponents.refreshAccountsButton.onDidClick(async () => {
|
||||||
|
await populateAzureAccounts();
|
||||||
|
}));
|
||||||
|
context.onNewDisposableCreated(accountComponents.signInButton.onDidClick(async () => {
|
||||||
|
await vscode.commands.executeCommand('workbench.actions.modal.linkedAccount');
|
||||||
|
await populateAzureAccounts();
|
||||||
|
}));
|
||||||
|
await populateAzureAccounts();
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAzureAccountDropdown(context: AzureAccountFieldContext): azdata.DropDownComponent {
|
function createAzureAccountDropdown(context: AzureAccountFieldContext): AzureAccountComponents {
|
||||||
const label = createLabel(context.view, {
|
const label = createLabel(context.view, {
|
||||||
text: loc.account,
|
text: loc.account,
|
||||||
description: context.fieldInfo.description,
|
description: context.fieldInfo.description,
|
||||||
@@ -771,8 +825,18 @@ function createAzureAccountDropdown(context: AzureAccountFieldContext): azdata.D
|
|||||||
});
|
});
|
||||||
accountDropdown.fireOnTextChange = true;
|
accountDropdown.fireOnTextChange = true;
|
||||||
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: accountDropdown });
|
context.onNewInputComponentCreated(context.fieldInfo.variableName!, { component: accountDropdown });
|
||||||
|
const signInButton = context.view!.modelBuilder.button().withProperties<azdata.ButtonProperties>({ label: loc.signIn, width: '100px' }).component();
|
||||||
|
const refreshButton = context.view!.modelBuilder.button().withProperties<azdata.ButtonProperties>({ label: loc.refresh, width: '100px' }).component();
|
||||||
addLabelInputPairToContainer(context.view, context.components, label, accountDropdown, context.fieldInfo);
|
addLabelInputPairToContainer(context.view, context.components, label, accountDropdown, context.fieldInfo);
|
||||||
return accountDropdown;
|
|
||||||
|
const buttons = createFlexContainer(context.view!, [signInButton, refreshButton], true, undefined, undefined, undefined, { 'margin-right': '10px' });
|
||||||
|
context.components.push(buttons);
|
||||||
|
return {
|
||||||
|
accountDropdown: accountDropdown,
|
||||||
|
signInButton: signInButton,
|
||||||
|
refreshAccountsButton: refreshButton
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAzureSubscriptionDropdown(
|
function createAzureSubscriptionDropdown(
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
|
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
|
||||||
import { IAccountManagementService } from 'sql/platform/accounts/common/interfaces';
|
import { IAccountManagementService } from 'sql/platform/accounts/common/interfaces';
|
||||||
|
|
||||||
CommandsRegistry.registerCommand('workbench.actions.modal.linkedAccount', accessor => {
|
CommandsRegistry.registerCommand('workbench.actions.modal.linkedAccount', async accessor => {
|
||||||
const accountManagementService = accessor.get(IAccountManagementService);
|
const accountManagementService = accessor.get(IAccountManagementService);
|
||||||
accountManagementService.openAccountListDialog();
|
await accountManagementService.openAccountListDialog();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ export class AccountManagementService implements IAccountManagementService {
|
|||||||
// UI METHODS //////////////////////////////////////////////////////////
|
// UI METHODS //////////////////////////////////////////////////////////
|
||||||
/**
|
/**
|
||||||
* Opens the account list dialog
|
* Opens the account list dialog
|
||||||
* @return Promise that finishes when the account list dialog opens
|
* @return Promise that finishes when the account list dialog closes
|
||||||
*/
|
*/
|
||||||
public openAccountListDialog(): Thenable<void> {
|
public openAccountListDialog(): Thenable<void> {
|
||||||
let self = this;
|
let self = this;
|
||||||
@@ -308,9 +308,8 @@ export class AccountManagementService implements IAccountManagementService {
|
|||||||
if (!self._accountDialogController) {
|
if (!self._accountDialogController) {
|
||||||
self._accountDialogController = self._instantiationService.createInstance(AccountDialogController);
|
self._accountDialogController = self._instantiationService.createInstance(AccountDialogController);
|
||||||
}
|
}
|
||||||
|
|
||||||
self._accountDialogController.openAccountDialog();
|
self._accountDialogController.openAccountDialog();
|
||||||
resolve();
|
self._accountDialogController.accountDialog.onCloseEvent(resolve);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(e);
|
reject(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import { InstantiationService } from 'vs/platform/instantiation/common/instantia
|
|||||||
import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
|
import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
|
||||||
import { EventVerifierSingle } from 'sql/base/test/common/event';
|
import { EventVerifierSingle } from 'sql/base/test/common/event';
|
||||||
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
|
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
|
||||||
|
import { AccountDialog } from 'sql/workbench/services/accountManagement/browser/accountDialog';
|
||||||
|
import { Emitter } from 'vs/base/common/event';
|
||||||
|
|
||||||
// SUITE CONSTANTS /////////////////////////////////////////////////////////
|
// SUITE CONSTANTS /////////////////////////////////////////////////////////
|
||||||
const hasAccountProvider: azdata.AccountProviderMetadata = {
|
const hasAccountProvider: azdata.AccountProviderMetadata = {
|
||||||
@@ -398,7 +400,14 @@ suite('Account Management Service Tests:', () => {
|
|||||||
|
|
||||||
// ... Add mocking for instantiating an account dialog controller
|
// ... Add mocking for instantiating an account dialog controller
|
||||||
let mockDialogController = TypeMoq.Mock.ofType(AccountDialogController);
|
let mockDialogController = TypeMoq.Mock.ofType(AccountDialogController);
|
||||||
|
let mockAccountDialog = {};
|
||||||
mockDialogController.setup(x => x.openAccountDialog());
|
mockDialogController.setup(x => x.openAccountDialog());
|
||||||
|
mockDialogController.setup(x => x.accountDialog).returns(() => <AccountDialog>mockAccountDialog);
|
||||||
|
let mockAccountDialogCloseEvent = new Emitter<void>();
|
||||||
|
mockAccountDialog['onCloseEvent'] = mockAccountDialogCloseEvent.event;
|
||||||
|
setTimeout(() => {
|
||||||
|
mockAccountDialogCloseEvent.fire();
|
||||||
|
}, 1000);
|
||||||
state.instantiationService.setup(x => x.createInstance(TypeMoq.It.isValue(AccountDialogController)))
|
state.instantiationService.setup(x => x.createInstance(TypeMoq.It.isValue(AccountDialogController)))
|
||||||
.returns(() => mockDialogController.object);
|
.returns(() => mockDialogController.object);
|
||||||
|
|
||||||
@@ -421,13 +430,25 @@ suite('Account Management Service Tests:', () => {
|
|||||||
|
|
||||||
// ... Add mocking for instantiating an account dialog controller
|
// ... Add mocking for instantiating an account dialog controller
|
||||||
let mockDialogController = TypeMoq.Mock.ofType(AccountDialogController);
|
let mockDialogController = TypeMoq.Mock.ofType(AccountDialogController);
|
||||||
|
let mockAccountDialog = {};
|
||||||
mockDialogController.setup(x => x.openAccountDialog());
|
mockDialogController.setup(x => x.openAccountDialog());
|
||||||
|
mockDialogController.setup(x => x.accountDialog).returns(() => <AccountDialog>mockAccountDialog);
|
||||||
|
let mockAccountDialogCloseEvent = new Emitter<void>();
|
||||||
|
mockAccountDialog['onCloseEvent'] = mockAccountDialogCloseEvent.event;
|
||||||
|
setTimeout(() => {
|
||||||
|
mockAccountDialogCloseEvent.fire();
|
||||||
|
}, 1000);
|
||||||
state.instantiationService.setup(x => x.createInstance(TypeMoq.It.isValue(AccountDialogController)))
|
state.instantiationService.setup(x => x.createInstance(TypeMoq.It.isValue(AccountDialogController)))
|
||||||
.returns(() => mockDialogController.object);
|
.returns(() => mockDialogController.object);
|
||||||
|
|
||||||
// If: I open the account dialog for a second time
|
// If: I open the account dialog for a second time
|
||||||
return state.accountManagementService.openAccountListDialog()
|
return state.accountManagementService.openAccountListDialog()
|
||||||
.then(() => state.accountManagementService.openAccountListDialog())
|
.then(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
mockAccountDialogCloseEvent.fire();
|
||||||
|
}, 1000);
|
||||||
|
state.accountManagementService.openAccountListDialog();
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Then:
|
// Then:
|
||||||
// ... The instantiation service should have only been called once
|
// ... The instantiation service should have only been called once
|
||||||
|
|||||||
Reference in New Issue
Block a user