From 83211d6e2d722d0ac4ef7ed1d277ace087b5c4b9 Mon Sep 17 00:00:00 2001 From: Alan Ren Date: Thu, 23 Jul 2020 17:54:32 -0700 Subject: [PATCH] deploy to single existing device (#11494) * deploy to single existing device * comments --- .../edge/deploy-sql-edge-single-device.ipynb | 365 ++++++++++++++++++ extensions/asde-deployment/package.json | 94 ++++- extensions/asde-deployment/package.nls.json | 14 +- 3 files changed, 467 insertions(+), 6 deletions(-) create mode 100644 extensions/asde-deployment/notebooks/edge/deploy-sql-edge-single-device.ipynb diff --git a/extensions/asde-deployment/notebooks/edge/deploy-sql-edge-single-device.ipynb b/extensions/asde-deployment/notebooks/edge/deploy-sql-edge-single-device.ipynb new file mode 100644 index 0000000000..077d291957 --- /dev/null +++ b/extensions/asde-deployment/notebooks/edge/deploy-sql-edge-single-device.ipynb @@ -0,0 +1,365 @@ +{ + "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": [ + "![Microsoft](https://raw.githubusercontent.com/microsoft/azuredatastudio/main/extensions/resource-deployment/images/microsoft-small-logo.png)\n", + "## Deploy Azure SQL Edge to an existing device via IoT hub\n", + "This notebook will walk you through process of deploying Azure SQL Edge to an existing device of an IoT hub:\n", + "1. Deploy Azure SQL Edge module to the device with optional dacpac\n", + "1. If a dacpac is selected, a storage account will be created to host the dacpac file\n", + "1. Enable connecting to the Azure SQL Edge instance on the device\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", + "iot_device_id = os.environ[\"AZDATA_NB_VAR_ASDE_DEVICE_ID\"]\n", + "ip_address = os.environ[\"AZDATA_NB_VAR_ASDE_DEVICE_IP_ADDRESS\"]\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'Device ID: {iot_device_id}')\n", + "print(f'Device IP address: {ip_address}')\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", + "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": [ + "### Make sure the IoT hub and device both exist" + ], + "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.')\r\n", + "\r\n", + "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)\r\n", + "device_list = [device for device in device_list if device['deviceId'] == iot_device_id]\r\n", + "if len(device_list) == 0:\r\n", + " sys.exit(f'Edge device \\\"{iot_device_id}\\\" 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 set-modules --device-id \\\"{iot_device_id}\\\" --hub-name \\\"{iot_hub_name}\\\" --content \\\"{file_name}\\\"')\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 Azure SQL Edge instance in Azure Data Studio**\n", + "Click the link below to connect to the Azure SQL Edge instance, 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", + "if ip_address == \"\":\n", + " print('Connect to Azure SQL Edge instance feature not available because device ip address is not provided')\n", + "else:\n", + " connectionParameter = '{\"serverName\":\"' + f'{ip_address},{sql_port}' + '\",\"providerName\":\"MSSQL\",\"authenticationType\":\"SqlLogin\",\"userName\": \"sa\",\"password\":' + json.dumps(sa_password) + '}'\n", + " display(HTML('
Click here to connect to the Azure SQL Edge instance
'))\n", + " display(HTML('
NOTE: The Azure SQL Edge instance password is included in this link, you may want to clear the results of this code cell before saving the notebook.'))" + ], + "metadata": { + "azdata_cell_guid": "8bc29cce-96a7-4a78-89af-5c73a6431c24", + "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 d4da5139f3..183dfa24b0 100644 --- a/extensions/asde-deployment/package.json +++ b/extensions/asde-deployment/package.json @@ -43,8 +43,12 @@ "displayName": "%sql-edge-remote-display-name%" }, { - "name": "azure", + "name": "azure-create-new", "displayName": "%sql-edge-azure-display-name%" + }, + { + "name": "azure-single-device", + "displayName": "%sql-edge-azure-single-device-display-name%" } ] } @@ -378,7 +382,93 @@ "name": "azure-cli" } ], - "when": "type=azure" + "when": "type=azure-create-new" + }, + { + "dialog": { + "notebook": "./notebooks/edge/deploy-sql-edge-single-device.ipynb", + "title": "%sql-edge-azure-single-device-title%", + "name": "sql-edge-azure-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-id%", + "variableName": "AZDATA_NB_VAR_ASDE_DEVICE_ID", + "type": "text", + "required": true + }, + { + "label": "%device-ip-address%", + "variableName": "AZDATA_NB_VAR_ASDE_DEVICE_IP_ADDRESS", + "type": "text", + "description": "%device-ip-address-description%", + "required": false + } + ] + }, + { + "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-single-device" } ], "agreement": { diff --git a/extensions/asde-deployment/package.nls.json b/extensions/asde-deployment/package.nls.json index 015c445af9..6ed7925cb6 100644 --- a/extensions/asde-deployment/package.nls.json +++ b/extensions/asde-deployment/package.nls.json @@ -25,8 +25,8 @@ "edge-remote-target-field": "Name or IP address", "edge-remote-username-field": "Username", "edge-remote-password-field": "Password", - "sql-edge-azure-display-name": "Azure VM via Azure IoT Hub", - "sql-edge-azure-title": "Deploy Azure SQL Edge to an Azure VM via IoT hub", + "sql-edge-azure-display-name": "New Azure IoT Hub and VM", + "sql-edge-azure-title": "Deploy Azure SQL Edge to a new Azure VM via IoT hub", "azure_subscription_id": "Subscription id", "azure_resource_group": "Resource group", "azure_location": "Location", @@ -35,6 +35,12 @@ "vm_password_confirm": "Confirm VM admin password", "dacpac_path": "Dacpac zip file", "azure-info-section-title": "Azure information", - "sqlserver-info-section-title": "SQL Server information", - "dacpac-zip-files": "Dacpac zip files" + "sqlserver-info-section-title": "Azure SQL Edge information", + "dacpac-zip-files": "Dacpac zip files", + "sql-edge-azure-single-device-display-name": "Existing device of an Azure IoT Hub", + "sql-edge-azure-single-device-title": "Deploy Azure SQL Edge to an existing device", + "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" }