diff --git a/extensions/asde-deployment/notebooks/edge/deploy-sql-edge-multi-device.ipynb b/extensions/asde-deployment/notebooks/edge/deploy-sql-edge-multi-device.ipynb
new file mode 100644
index 0000000000..6fcd7076aa
--- /dev/null
+++ b/extensions/asde-deployment/notebooks/edge/deploy-sql-edge-multi-device.ipynb
@@ -0,0 +1,327 @@
+{
+ "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",
+ "## Deploy Azure SQL Edge to multiple devices via IoT hub\n",
+ "This notebook will walk you through process of deploying Azure SQL Edge to multiple devices of an IoT hub.\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",
+ "Please press the \"Run all\" button to run the notebook"
+ ],
+ "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",
+ "sa_password = os.environ[\"AZDATA_NB_VAR_SA_PASSWORD\"]\n",
+ "dacpac_path = os.environ[\"AZDATA_NB_VAR_ASDE_DACPAC_PATH\"]\n",
+ "sql_port = os.environ[\"AZDATA_NB_VAR_ASDE_SQL_PORT\"]\n",
+ "iot_hub_name = os.environ[\"AZDATA_NB_VAR_ASDE_HUBNAME\"]\n",
+ "target_condition = os.environ[\"AZDATA_NB_VAR_ASDE_TARGET_CONDITION\"]\n",
+ "print(f'Subscription: {azure_subscription_id}')\n",
+ "print(f'Resource group: {azure_resource_group}')\n",
+ "print(f'IoT hub name: {iot_hub_name}')\n",
+ "print(f'Target condition: {target_condition}')\n",
+ "print(f'Azure SQL Edge instance port: {sql_port}')\n",
+ "print(f'Azure SQL Edge instance 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": [
+ "suffix = time.strftime(\"%y%m%d%H%M%S\", time.localtime())\n",
+ "azure_storage_account = f'sa{suffix}'\n",
+ "deployment_id = f'asde{suffix}'\n",
+ "storage_account_container = 'sqldatabasedacpac'\n",
+ "sql_lcid = '1033'\n",
+ "sql_collation = 'SQL_Latin1_General_CP1_CI_AS'\n",
+ "deployment_priority = 10"
+ ],
+ "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": [
+ "### Make sure the IoT hub exists"
+ ],
+ "metadata": {
+ "azdata_cell_guid": "b207680d-a0df-49b5-9e9e-5fd8c3d492d3"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "hub_list = run_command(f'az iot hub list --resource-group {azure_resource_group}', returnObject=True)\r\n",
+ "hub_list = [hub for hub in hub_list if hub['name'] == iot_hub_name]\r\n",
+ "if len(hub_list) == 0:\r\n",
+ " sys.exit(f'IoT hub \\\"{iot_hub_name}\\\" does not exist.')"
+ ],
+ "metadata": {
+ "azdata_cell_guid": "38798c56-b2a0-4af8-b39e-5e26f3c79aeb",
+ "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} --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": [
+ "### Deploy Azure SQL 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\\\\\\\":\\\\\\\"\\\\\\\"}]},\\\\\\\"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\\\":\\\"\\\"},\\\"MSSQL_LCID\\\":{\\\"value\\\":\\\"\\\"},\\\"MSSQL_COLLATION\\\":{\\\"value\\\":\\\"\\\"}},\\\"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\\\":\\\"\\\",\\\"ASAJobInfo\\\":\\\"\\\"}}}}'\n",
+ "manifest = manifest.replace('', blob_sas).replace('',sa_password).replace('',sql_lcid).replace('',sql_port).replace('',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 deployment create --target-condition \\\"{target_condition}\\\" --hub-name \\\"{iot_hub_name}\\\" --content \\\"{file_name}\\\" --deployment-id \\\"{deployment_id}\\\" --priority {deployment_priority}')\n",
+ "os.remove(file_name)"
+ ],
+ "metadata": {
+ "azdata_cell_guid": "81a86ff6-5a83-48be-8be7-654d152eea89",
+ "tags": [
+ "hide_input"
+ ]
+ },
+ "outputs": [],
+ "execution_count": null
+ }
+ ]
+}
\ No newline at end of file
diff --git a/extensions/asde-deployment/package.json b/extensions/asde-deployment/package.json
index 183dfa24b0..af8d7f4db6 100644
--- a/extensions/asde-deployment/package.json
+++ b/extensions/asde-deployment/package.json
@@ -49,6 +49,10 @@
{
"name": "azure-single-device",
"displayName": "%sql-edge-azure-single-device-display-name%"
+ },
+ {
+ "name": "azure-multi-device",
+ "displayName": "%sql-edge-azure-multi-device-display-name%"
}
]
}
@@ -388,7 +392,7 @@
"dialog": {
"notebook": "./notebooks/edge/deploy-sql-edge-single-device.ipynb",
"title": "%sql-edge-azure-single-device-title%",
- "name": "sql-edge-azure-dialog",
+ "name": "sql-edge-azure-single-device-dialog",
"tabs": [
{
"title": "",
@@ -469,6 +473,95 @@
}
],
"when": "type=azure-single-device"
+ },
+ {
+ "dialog": {
+ "notebook": "./notebooks/edge/deploy-sql-edge-multi-device.ipynb",
+ "title": "%sql-edge-azure-multi-device-title%",
+ "name": "sql-edge-azure-multi-device-dialog",
+ "tabs": [
+ {
+ "title": "",
+ "sections": [
+ {
+ "title": "%azure-info-section-title%",
+ "collapsible": true,
+ "fields": [
+ {
+ "type": "azure_account",
+ "subscriptionVariableName": "AZDATA_NB_VAR_ASDE_SUBSCRIPTIONID",
+ "resourceGroupVariableName": "AZDATA_NB_VAR_ASDE_RESOURCEGROUP",
+ "required": true
+ },
+ {
+ "label": "%iot-hub-name%",
+ "variableName": "AZDATA_NB_VAR_ASDE_HUBNAME",
+ "type": "text",
+ "required": true
+ },
+ {
+ "label": "%device-target-condition%",
+ "variableName": "AZDATA_NB_VAR_ASDE_TARGET_CONDITION",
+ "type": "text",
+ "required": true
+ },
+ {
+ "type": "readonly_text",
+ "label": "{0}",
+ "links": [
+ {
+ "text": "%device-target-condition-learn-more%",
+ "url": "https://docs.microsoft.com/azure/iot-edge/module-deployment-monitoring#target-condition"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "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-multi-device"
}
],
"agreement": {
diff --git a/extensions/asde-deployment/package.nls.json b/extensions/asde-deployment/package.nls.json
index 6ed7925cb6..e37699bb2f 100644
--- a/extensions/asde-deployment/package.nls.json
+++ b/extensions/asde-deployment/package.nls.json
@@ -42,5 +42,9 @@
"iot-hub-name": "IoT Hub name",
"device-id": "Device ID",
"device-ip-address": "Device IP Address",
- "device-ip-address-description": "Will be used to connect to the Azure SQL Edge instance after deployment"
+ "device-ip-address-description": "Will be used to connect to the Azure SQL Edge instance after deployment",
+ "sql-edge-azure-multi-device-display-name": "Multiple devices of an Azure IoT Hub",
+ "sql-edge-azure-multi-device-title": "Deploy Azure SQL Edge to multiple Azure IoT devices",
+ "device-target-condition": "Target condition",
+ "device-target-condition-learn-more": "Learn more about target condition"
}