diff --git a/build/gulpfile.hygiene.js b/build/gulpfile.hygiene.js
index 61fe93e6d4..f0b867af0a 100644
--- a/build/gulpfile.hygiene.js
+++ b/build/gulpfile.hygiene.js
@@ -104,6 +104,7 @@ const indentationFilter = [
'!extensions/admin-tool-ext-win/ssmsmin/**',
'!extensions/resource-deployment/notebooks/**',
'!extensions/mssql/notebooks/**',
+ '!extensions/azurehybridtoolkit/notebooks/**',
'!extensions/integration-tests/testData/**',
'!extensions/arc/src/controller/generated/**',
'!extensions/sql-database-projects/resources/templates/*.xml',
@@ -178,6 +179,7 @@ const copyrightFilter = [
'!extensions/mssql/src/prompts/**',
'!extensions/kusto/src/prompts/**',
'!extensions/notebook/resources/jupyter_config/**',
+ '!extensions/azurehybridtoolkit/notebooks/**',
'!extensions/query-history/images/**',
'!extensions/sql/build/update-grammar.js',
'!**/*.gif',
diff --git a/extensions/azurehybridtoolkit/.vscodeignore b/extensions/azurehybridtoolkit/.vscodeignore
new file mode 100644
index 0000000000..cc8c39789b
--- /dev/null
+++ b/extensions/azurehybridtoolkit/.vscodeignore
@@ -0,0 +1,4 @@
+src/**
+out/**
+tsconfig.json
+yarn.lock
diff --git a/extensions/azurehybridtoolkit/README.md b/extensions/azurehybridtoolkit/README.md
new file mode 100644
index 0000000000..edede77739
--- /dev/null
+++ b/extensions/azurehybridtoolkit/README.md
@@ -0,0 +1,17 @@
+# Azure SQL Hybrid Cloud Toolkit Jupyter Book Extension for Azure Data Studio
+
+Welcome to the Azure SQL Hybrid Cloud Toolkit Jupyter Book Extension for Azure Data Studio! This extension opens a Jupyter Book that has several utilities for Azure SQL such as migration assessments and setting up networking connectivity.
+
+## Code of Conduct
+
+This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
+
+## Privacy Statement
+
+The [Microsoft Enterprise and Developer Privacy Statement](https://privacy.microsoft.com/en-us/privacystatement) describes the privacy statement of this software.
+
+## License
+
+Copyright (c) Microsoft Corporation. All rights reserved.
+
+Licensed under the [Source EULA](https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt).
diff --git a/extensions/azurehybridtoolkit/extension.webpack.config.js b/extensions/azurehybridtoolkit/extension.webpack.config.js
new file mode 100644
index 0000000000..5eb11780d2
--- /dev/null
+++ b/extensions/azurehybridtoolkit/extension.webpack.config.js
@@ -0,0 +1,17 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the Source EULA. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+//@ts-check
+
+'use strict';
+
+const withDefaults = require('../shared.webpack.config');
+
+module.exports = withDefaults({
+ context: __dirname,
+ entry: {
+ main: './src/main.ts'
+ },
+});
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl.ipynb b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl.ipynb
new file mode 100644
index 0000000000..99c3f85eb4
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl.ipynb
@@ -0,0 +1,161 @@
+{
+ "metadata": {
+ "kernelspec": {
+ "name": "powershell",
+ "display_name": "PowerShell"
+ },
+ "language_info": {
+ "name": "powershell",
+ "codemirror_mode": "shell",
+ "mimetype": "text/x-sh",
+ "file_extension": ".ps1"
+ }
+ },
+ "nbformat_minor": 2,
+ "nbformat": 4,
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "source": [
+ "# 0. Preparing the Credential"
+ ],
+ "metadata": {
+ "azdata_cell_guid": "140ce3a4-9596-47b5-ad22-87c3bd2057f6"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "$functionKey = 'yourAzureFunctionKey'\r\n",
+ "$Login = 'yourSqlServerLogin'\r\n",
+ "$Password = 'yourSqlServerPassword'\r\n",
+ "\r\n",
+ "$headers = @{\r\n",
+ " 'x-functions-key' = $functionKey\r\n",
+ "}"
+ ],
+ "metadata": {
+ "azdata_cell_guid": "36fa6902-7640-462d-bc2e-6b49e9aaa0d9",
+ "tags": []
+ },
+ "outputs": [],
+ "execution_count": null
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "# 1. Calling the ADP Orchestrator (Export)\r\n",
+ "## 1.1 Submit the Export request to the source Azure SQL Server."
+ ],
+ "metadata": {
+ "azdata_cell_guid": "40c4517b-8145-4af1-bbbb-3be3b9b9a8a0"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "$Url = 'https://adpcontrol.azurewebsites.net/api/subscriptions/0009fc4d-e310-4e40-8e63-c48a23e9cdc1/resourceGroups/seanadp01/Export'\n",
+ "\n",
+ "$Body = @{\n",
+ " batchAccountUrl = 'https://adp.eastus.batch.azure.com'\n",
+ " storageAccountName = 'adp01batch'\n",
+ " sourceSqlServerResourceGroupName = 'SeanADP01Source'\n",
+ " sourceSqlServerName = 'adpsvr01'\n",
+ " userName = $Login \n",
+ " password = $Password \n",
+ "}\n",
+ "\n",
+ "$json = $Body | ConvertTo-Json\n",
+ "$exportResponse = Invoke-RestMethod -Method 'Post' -Headers $headers -Uri $Url -Body $json -ContentType 'application/json'\n",
+ "$exportResponse"
+ ],
+ "metadata": {
+ "azdata_cell_guid": "7e1d3261-5e61-4106-8063-7cd58ffd0cf1",
+ "tags": []
+ },
+ "outputs": [],
+ "execution_count": null
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## 1.2 Getting the Operation Status"
+ ],
+ "metadata": {
+ "azdata_cell_guid": "52204c15-abad-4ce5-8629-d290332f730b"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "Invoke-RestMethod -Method 'Get' -Uri $exportResponse.statusQueryGetUri"
+ ],
+ "metadata": {
+ "azdata_cell_guid": "a0ba1261-3a26-4168-b149-1b9e44939432",
+ "tags": [
+ "hide_input"
+ ]
+ },
+ "outputs": [],
+ "execution_count": null
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "# 2. Calling the ADP Orchestrator (Import)\r\n",
+ "## 2.1 Submit the Import request to the target Azure SQL Server."
+ ],
+ "metadata": {
+ "azdata_cell_guid": "2c862275-c380-476a-ab3d-a9aacdca963b"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "$Url = 'https://adpcontrol.azurewebsites.net/api/subscriptions/0009fc4d-e310-4e40-8e63-c48a23e9cdc1/resourceGroups/seanadp01/Import'\r\n",
+ "$Body = @{\r\n",
+ " batchAccountUrl = 'https://adp.eastus.batch.azure.com'\r\n",
+ " storageAccountName = 'adp01batch'\r\n",
+ " containerName = 'adpsvr01-0428061710'\r\n",
+ " targetSqlServerResourceGroupName = 'SeanADP01Target'\r\n",
+ " targetSqlServerName = 'adpsvr03'\r\n",
+ " userName = $Login \r\n",
+ " password = $Password \r\n",
+ "}\r\n",
+ "\r\n",
+ "$json = $Body | ConvertTo-Json\r\n",
+ "$importResponse = Invoke-RestMethod -Method 'Post' -Headers $headers -Uri $Url -Body $json -ContentType 'application/json'\r\n",
+ "$importResponse"
+ ],
+ "metadata": {
+ "azdata_cell_guid": "315859aa-e452-4ab3-acb5-92c7c8bd5857",
+ "tags": []
+ },
+ "outputs": [],
+ "execution_count": null
+ },
+ {
+ "cell_type": "markdown",
+ "source": [
+ "## 2.2 Getting the Operation Status"
+ ],
+ "metadata": {
+ "azdata_cell_guid": "5da3293b-7d10-4315-8106-79e56cd657ea"
+ }
+ },
+ {
+ "cell_type": "code",
+ "source": [
+ "Invoke-RestMethod -Method 'Get' -Uri $importResponse.statusQueryGetUri"
+ ],
+ "metadata": {
+ "azdata_cell_guid": "328d3a27-4cdf-4623-a8c5-8230487efbed",
+ "tags": [
+ "hide_input"
+ ]
+ },
+ "outputs": [],
+ "execution_count": null
+ }
+ ]
+}
\ No newline at end of file
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl.sln b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl.sln
new file mode 100644
index 0000000000..207c24511b
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl.sln
@@ -0,0 +1,37 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.29920.165
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ADPControl", "ADPControl\ADPControl.csproj", "{6309D4C5-F118-4C89-A67E-E557CA41ABA2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BatchWrapper", "BatchWrapper\BatchWrapper.csproj", "{E64CD92E-2D2C-48F1-B6B1-A7AF819B06F1}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SqlPackageWrapper", "SqlPackageWrapper\SqlPackageWrapper.csproj", "{A19335D3-9D80-43BE-9351-C3C3704689B0}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {6309D4C5-F118-4C89-A67E-E557CA41ABA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6309D4C5-F118-4C89-A67E-E557CA41ABA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6309D4C5-F118-4C89-A67E-E557CA41ABA2}.Release|Any CPU.ActiveCfg = Debug|Any CPU
+ {6309D4C5-F118-4C89-A67E-E557CA41ABA2}.Release|Any CPU.Build.0 = Debug|Any CPU
+ {E64CD92E-2D2C-48F1-B6B1-A7AF819B06F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E64CD92E-2D2C-48F1-B6B1-A7AF819B06F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E64CD92E-2D2C-48F1-B6B1-A7AF819B06F1}.Release|Any CPU.ActiveCfg = Debug|Any CPU
+ {E64CD92E-2D2C-48F1-B6B1-A7AF819B06F1}.Release|Any CPU.Build.0 = Debug|Any CPU
+ {A19335D3-9D80-43BE-9351-C3C3704689B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A19335D3-9D80-43BE-9351-C3C3704689B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A19335D3-9D80-43BE-9351-C3C3704689B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A19335D3-9D80-43BE-9351-C3C3704689B0}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {3DB5F0CB-1D26-4E33-801D-7BBAE823C39D}
+ EndGlobalSection
+EndGlobal
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl/ADPControl.csproj b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl/ADPControl.csproj
new file mode 100644
index 0000000000..730cc4b42b
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl/ADPControl.csproj
@@ -0,0 +1,26 @@
+
+
+ netcoreapp3.1
+ v3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+ Never
+
+
+
\ No newline at end of file
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl/AzureResourceManagerActivity.cs b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl/AzureResourceManagerActivity.cs
new file mode 100644
index 0000000000..21830d76d1
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl/AzureResourceManagerActivity.cs
@@ -0,0 +1,244 @@
+using Microsoft.Azure.Management.ResourceManager;
+using Microsoft.Azure.Management.ResourceManager.Models;
+using Microsoft.Azure.Management.Storage;
+using Microsoft.Azure.Management.Storage.Models;
+using Microsoft.Azure.Services.AppAuthentication;
+using Microsoft.Azure.WebJobs;
+using Microsoft.Azure.WebJobs.Extensions.DurableTask;
+using Microsoft.Extensions.Logging;
+using Microsoft.Rest;
+using Microsoft.WindowsAzure.Storage;
+using Microsoft.WindowsAzure.Storage.Auth;
+using Microsoft.WindowsAzure.Storage.Blob;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using static ADPControl.HttpSurface;
+
+namespace ADPControl
+{
+ public static class AzureResourceManagerActivity
+ {
+ private const string ArmTemplateFileName = "template.json";
+
+ private static string[] AllowedImportSubServerResourceTypes = new string[] {
+ "Microsoft.Sql/servers/firewallRules",
+ "Microsoft.Sql/servers/databases",
+ "Microsoft.Sql/servers/elasticPools",
+ //"Microsoft.Sql/servers/keys",
+ //"Microsoft.Sql/servers/databases/transparentDataEncryption",
+ "Microsoft.Sql/servers/databases/backupShortTermRetentionPolicies",
+ "Microsoft.Sql/servers/administrators"
+ };
+
+ // Deploy the ARM template
+ [FunctionName(nameof(BeginDeployArmTemplateForImport))]
+ public static async Task BeginDeployArmTemplateForImport([ActivityTrigger] ImportRequest request, ILogger log)
+ {
+ var azureServiceTokenProvider = new AzureServiceTokenProvider();
+ TokenCredentials tokenArmCredential = new TokenCredentials(await azureServiceTokenProvider.GetAccessTokenAsync("https://management.core.windows.net/"));
+
+ ResourceManagementClient resourcesClient = new ResourceManagementClient(tokenArmCredential) { SubscriptionId = request.SubscriptionId.ToString() };
+ StorageManagementClient storageMgmtClient = new StorageManagementClient(tokenArmCredential) { SubscriptionId = request.SubscriptionId.ToString() };
+
+ // Get the storage account keys for a given account and resource group
+ IList acctKeys = storageMgmtClient.StorageAccounts.ListKeys(request.ResourceGroupName, request.StorageAccountName).Keys;
+
+ // Get a Storage account using account creds:
+ StorageCredentials storageCred = new StorageCredentials(request.StorageAccountName, acctKeys.FirstOrDefault().Value);
+ CloudStorageAccount linkedStorageAccount = new CloudStorageAccount(storageCred, true);
+ CloudBlobContainer container = linkedStorageAccount
+ .CreateCloudBlobClient()
+ .GetContainerReference(request.ContainerName);
+
+ CloudBlockBlob blob = container.GetBlockBlobReference(ArmTemplateFileName);
+ string json = await blob.DownloadTextAsync();
+
+ JObject originalTemplate = JObject.Parse(json);
+ JObject importTemplate = UpdateArmTemplateForImport(originalTemplate, request);
+
+ var deployParams = new Deployment
+ {
+ Properties = new DeploymentProperties
+ {
+ Mode = DeploymentMode.Incremental,
+ Template = importTemplate
+ }
+ };
+
+ string deploymentName = request.TargetSqlServerName + "_" + DateTime.UtcNow.ToFileTimeUtc();
+
+ try
+ {
+ await resourcesClient.Deployments.BeginCreateOrUpdateAsync(request.TargetSqlServerResourceGroupName, deploymentName, deployParams);
+ }
+ catch (Exception ex)
+ {
+ log.LogError(ex.ToString());
+ throw ex;
+ }
+
+ return deploymentName;
+ }
+
+ // Get the ARM deployment status
+ [FunctionName(nameof(GetArmDeploymentForImport))]
+ public static async Task GetArmDeploymentForImport([ActivityTrigger] (Guid, string, string) input)
+ {
+ Guid subscriptionId = input.Item1;
+ string resourceGroupName = input.Item2;
+ string deploymentName = input.Item3;
+
+ var azureServiceTokenProvider = new AzureServiceTokenProvider();
+ TokenCredentials tokenArmCredential = new TokenCredentials(await azureServiceTokenProvider.GetAccessTokenAsync("https://management.core.windows.net/"));
+ ResourceManagementClient resourcesClient = new ResourceManagementClient(tokenArmCredential) { SubscriptionId = subscriptionId.ToString() };
+
+ DeploymentExtended result = await resourcesClient.Deployments.GetAsync(resourceGroupName, deploymentName);
+ return result.Properties.ProvisioningState;
+ }
+
+ // Get the ARM template without the parameter of the resource name
+ [FunctionName(nameof(GetArmTemplateForExportSkipParameterization))]
+ public static async Task GetArmTemplateForExportSkipParameterization([ActivityTrigger] ExportRequest request, ILogger log)
+ {
+ log.LogInformation("GetArmTemplateForExportSkipParameterization: entering");
+
+ var azureServiceTokenProvider = new AzureServiceTokenProvider();
+ TokenCredentials tokenArmCredential = new TokenCredentials(await azureServiceTokenProvider.GetAccessTokenAsync("https://management.core.windows.net/"));
+ if (tokenArmCredential != null)
+ {
+ log.LogInformation("GetArmTemplateForExportSkipParameterization: acquired access token");
+ ResourceManagementClient resourcesClient = new ResourceManagementClient(tokenArmCredential) { SubscriptionId = request.SubscriptionId.ToString() };
+
+ string sourceSqlServerResourceId = string.Format("/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Sql/servers/{2}", request.SubscriptionId, request.SourceSqlServerResourceGroupName, request.SourceSqlServerName);
+ ResourceGroupExportResult exportedTemplate = resourcesClient.ResourceGroups.ExportTemplate(request.SourceSqlServerResourceGroupName, new ExportTemplateRequest(new List { sourceSqlServerResourceId }, "SkipResourceNameParameterization"));
+
+ log.LogInformation("GetArmTemplateForExportSkipParameterization: server template exported. Size: {0} bytes", exportedTemplate.Template.ToString().Length);
+ dynamic template = (dynamic)exportedTemplate.Template;
+
+ // Filtering the list of databases
+ dynamic databases = template.resources.SelectTokens("$.[?(@.type == 'Microsoft.Sql/servers/databases')]");
+ int numberOfDatabases = 0;
+ foreach (var db in databases)
+ {
+ numberOfDatabases++;
+ }
+ log.LogInformation("GetArmTemplateForExportSkipParameterization: exiting with database list. Databases count: {0}", numberOfDatabases);
+
+ return databases;
+ }
+
+ log.LogInformation("GetArmTemplateForExportSkipParameterization: exiting with empty database list");
+ return null;
+ }
+
+ // Get the ARM template without the parameter of the resource name
+ [FunctionName(nameof(GetArmTemplateForImportSkipParameterization))]
+ public static async Task GetArmTemplateForImportSkipParameterization([ActivityTrigger] ImportRequest request, ILogger log)
+ {
+ var azureServiceTokenProvider = new AzureServiceTokenProvider();
+ TokenCredentials tokenArmCredential = new TokenCredentials(await azureServiceTokenProvider.GetAccessTokenAsync("https://management.core.windows.net/"));
+ if (tokenArmCredential != null)
+ {
+ log.LogInformation("GetArmTemplateForImportSkipParameterization: acquired access token");
+ ResourceManagementClient resourcesClient = new ResourceManagementClient(tokenArmCredential) { SubscriptionId = request.SubscriptionId.ToString() };
+
+ string sourceSqlServerResourceId = string.Format("/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Sql/servers/{2}", request.SubscriptionId, request.TargetSqlServerResourceGroupName, request.TargetSqlServerName);
+ ResourceGroupExportResult exportedTemplate = resourcesClient.ResourceGroups.ExportTemplate(request.TargetSqlServerResourceGroupName, new ExportTemplateRequest(new List { sourceSqlServerResourceId }, "SkipResourceNameParameterization"));
+
+ log.LogInformation("GetArmTemplateForImportSkipParameterization: server template exported. Size: {0} bytes", exportedTemplate.Template.ToString().Length);
+ dynamic template = (dynamic)exportedTemplate.Template;
+
+ // Filtering the list of databases
+ dynamic databases = template.resources.SelectTokens("$.[?(@.type == 'Microsoft.Sql/servers/databases')]");
+
+ int numberOfDatabases = 0;
+ foreach (var db in databases)
+ {
+ numberOfDatabases++;
+ }
+ log.LogInformation("GetArmTemplateForExportSkipParameterization: exiting with database list. Databases count: {0}", numberOfDatabases);
+ return databases;
+ }
+
+ return null;
+ }
+
+ [FunctionName(nameof(GetArmTemplateForExport))]
+ public static async Task GetArmTemplateForExport([ActivityTrigger] ExportRequest request)
+ {
+ var azureServiceTokenProvider = new AzureServiceTokenProvider();
+ TokenCredentials tokenArmCredential = new TokenCredentials(await azureServiceTokenProvider.GetAccessTokenAsync("https://management.core.windows.net/"));
+
+ ResourceManagementClient resourcesClient = new ResourceManagementClient(tokenArmCredential) { SubscriptionId = request.SubscriptionId.ToString() };
+
+ string sourceSqlServerResourceId = string.Format("/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Sql/servers/{2}", request.SubscriptionId, request.SourceSqlServerResourceGroupName, request.SourceSqlServerName);
+ ResourceGroupExportResult exportedTemplate = resourcesClient.ResourceGroups.ExportTemplate(request.SourceSqlServerResourceGroupName, new ExportTemplateRequest(new List { sourceSqlServerResourceId }, "IncludeParameterDefaultValue"));
+ return exportedTemplate.Template;
+ }
+
+ private static JObject UpdateArmTemplateForImport(JObject originalTemplate, ImportRequest request)
+ {
+ string serverNameParameterName = null;
+
+ // Go through every parameter to find the property name is like 'server_%_name'
+ using (JsonTextReader reader = new JsonTextReader(new StringReader(originalTemplate["parameters"].ToString())))
+ {
+ while (reader.Read())
+ {
+ if (reader.TokenType.ToString().Equals("PropertyName")
+ && reader.ValueType.ToString().Equals("System.String")
+ && reader.Value.ToString().StartsWith("servers_")
+ && reader.Value.ToString().EndsWith("_name"))
+ {
+ serverNameParameterName = reader.Value.ToString();
+ break;
+ }
+ }
+ }
+
+ // 1. Replacing the default value to the target server name, appending to the new template
+ originalTemplate["parameters"][serverNameParameterName]["defaultValue"] = request.TargetSqlServerName;
+ JObject serverNameParameterValue = (JObject)originalTemplate["parameters"][serverNameParameterName];
+
+ // 2. Cleanup all the parameters except the updated server name
+ ((JObject)originalTemplate["parameters"]).RemoveAll();
+ ((JObject)originalTemplate["parameters"]).Add(serverNameParameterName, serverNameParameterValue);
+
+ // 3. Adjust the servers resource by adding password after the login
+ JObject server = (JObject)originalTemplate["resources"]
+ .SelectToken("$.[?(@.type == 'Microsoft.Sql/servers')]");
+
+ server.Remove("identity");
+
+ JObject serverProperties = (JObject)server["properties"];
+ serverProperties.Property("administratorLogin")
+ .AddAfterSelf(new JProperty("administratorLoginPassword", request.SqlAdminPassword));
+
+ JArray newResources = new JArray();
+
+ // 4. Getting the whitelisted resources and adding them to the new template later.
+ foreach (string resourceType in AllowedImportSubServerResourceTypes)
+ {
+ List resources = originalTemplate["resources"]
+ .SelectTokens(string.Format("$.[?(@.type == '{0}')]", resourceType)).ToList();
+ newResources.Add(resources);
+ }
+
+ // 5. Clean up all the resources excepted the new server and whitelisted resource type.
+ ((JArray)originalTemplate["resources"]).Clear();
+ ((JArray)originalTemplate["resources"]).Add(server);
+
+ foreach (var resource in newResources)
+ {
+ ((JArray)originalTemplate["resources"]).Add(resource);
+ }
+
+ return originalTemplate;
+ }
+ }
+}
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl/BatchActivity.cs b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl/BatchActivity.cs
new file mode 100644
index 0000000000..2b84b66c99
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl/BatchActivity.cs
@@ -0,0 +1,214 @@
+using Microsoft.Azure.Batch;
+using Microsoft.Azure.Batch.Auth;
+using Microsoft.Azure.Batch.Common;
+using Microsoft.Azure.Services.AppAuthentication;
+using Microsoft.Azure.WebJobs;
+using Microsoft.Azure.WebJobs.Extensions.DurableTask;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using static ADPControl.HttpSurface;
+
+namespace ADPControl
+{
+ public static class BatchActivity
+ {
+ // Batch resource settings
+ private const string PoolVMSize = "Standard_D8s_v3";
+ private const string PoolId = PoolVMSize;
+ private const int PoolNodeCount = 2;
+ private const string AppPackageName = "SqlPackageWrapper";
+ public const string AppPackageVersion = "1";
+
+ [FunctionName(nameof(CreateBatchPoolAndExportJob))]
+ public static async Task CreateBatchPoolAndExportJob([ActivityTrigger] ExportRequest request, ILogger log)
+ {
+ var azureServiceTokenProvider = new AzureServiceTokenProvider();
+
+ // Get a Batch client using function identity
+ BatchTokenCredentials batchCred = new BatchTokenCredentials(request.BatchAccountUrl, await azureServiceTokenProvider.GetAccessTokenAsync("https://batch.core.windows.net/"));
+
+ string jobId = request.SourceSqlServerName + "-Export-" + DateTime.UtcNow.ToString("MMddHHmmss");
+ using (BatchClient batchClient = BatchClient.Open(batchCred))
+ {
+ ImageReference imageReference = CreateImageReference();
+ VirtualMachineConfiguration vmConfiguration = CreateVirtualMachineConfiguration(imageReference);
+
+ await CreateBatchPoolIfNotExist(batchClient, vmConfiguration, request.VNetSubnetId);
+ await CreateBatchJob(batchClient, jobId, log);
+ }
+
+ return jobId;
+ }
+
+ [FunctionName(nameof(CreateBatchPoolAndImportJob))]
+ public static async Task CreateBatchPoolAndImportJob([ActivityTrigger] ImportRequest request, ILogger log)
+ {
+ var azureServiceTokenProvider = new AzureServiceTokenProvider();
+
+ // Get a Batch client using function identity
+ BatchTokenCredentials batchCred = new BatchTokenCredentials(request.BatchAccountUrl, await azureServiceTokenProvider.GetAccessTokenAsync("https://batch.core.windows.net/"));
+
+ string jobId = request.TargetSqlServerName + "-Import-" + DateTime.UtcNow.ToString("MMddHHmmss");
+ using (BatchClient batchClient = BatchClient.Open(batchCred))
+ {
+ ImageReference imageReference = CreateImageReference();
+ VirtualMachineConfiguration vmConfiguration = CreateVirtualMachineConfiguration(imageReference);
+
+ await CreateBatchPoolIfNotExist(batchClient, vmConfiguration, request.VNetSubnetId);
+ await CreateBatchJob(batchClient, jobId, log);
+ }
+
+ return jobId;
+ }
+
+ public static async Task CreateBatchJob(BatchClient batchClient, string jobId, ILogger log)
+ {
+ // Create a Batch job
+ log.LogInformation("Creating job [{0}]...", jobId);
+ CloudJob job = null;
+
+ try
+ {
+ job = batchClient.JobOperations.CreateJob(jobId, new PoolInformation { PoolId = PoolId });
+ job.OnAllTasksComplete = OnAllTasksComplete.TerminateJob;
+
+ // Commit the job to the Batch service
+ await job.CommitAsync();
+
+ log.LogInformation($"Created job {jobId}");
+
+ // Obtain the bound job from the Batch service
+ await job.RefreshAsync();
+ }
+ catch (BatchException be)
+ {
+ // Accept the specific error code JobExists as that is expected if the job already exists
+ if (be.RequestInformation?.BatchError?.Code == BatchErrorCodeStrings.JobExists)
+ {
+ log.LogWarning("The job {0} already existed when we tried to create it", jobId);
+ }
+ else
+ {
+ log.LogError("Exception creating job: {0}", be.Message);
+ throw be; // Any other exception is unexpected
+ }
+ }
+
+ return job;
+ }
+
+ // Create the Compute Pool of the Batch Account
+ public static async Task CreateBatchPoolIfNotExist(BatchClient batchClient, VirtualMachineConfiguration vmConfiguration, string vnetSubnetId)
+ {
+ Console.WriteLine("Creating pool [{0}]...", PoolId);
+
+ try
+ {
+ CloudPool pool = batchClient.PoolOperations.CreatePool(
+ poolId: PoolId,
+ targetDedicatedComputeNodes: PoolNodeCount,
+ virtualMachineSize: PoolVMSize,
+ virtualMachineConfiguration: vmConfiguration);
+
+ // Specify the application and version to install on the compute nodes
+ pool.ApplicationPackageReferences = new List
+ {
+ new ApplicationPackageReference {
+ ApplicationId = AppPackageName,
+ Version = AppPackageVersion }
+ };
+
+ // Initial the first data disk for each VM in the pool
+ StartTask startTask = new StartTask("cmd /c Powershell -command \"Get-Disk | Where partitionstyle -eq 'raw' | sort number | Select-Object -first 1 |" +
+ " Initialize-Disk -PartitionStyle MBR -PassThru | New-Partition -UseMaximumSize -DriveLetter F |" +
+ " Format-Volume -FileSystem NTFS -NewFileSystemLabel data1 -Confirm:$false -Force\"");
+
+ startTask.MaxTaskRetryCount = 1;
+ startTask.UserIdentity = new UserIdentity(new AutoUserSpecification(AutoUserScope.Pool, ElevationLevel.Admin));
+ startTask.WaitForSuccess = true;
+
+ pool.StartTask = startTask;
+
+ // Create the Pool within the vnet subnet if it's specified.
+ if (vnetSubnetId != null)
+ {
+ pool.NetworkConfiguration = new NetworkConfiguration();
+ pool.NetworkConfiguration.SubnetId = vnetSubnetId;
+ }
+
+ await pool.CommitAsync();
+ await pool.RefreshAsync();
+ }
+ catch (BatchException be)
+ {
+ // Accept the specific error code PoolExists as that is expected if the pool already exists
+ if (be.RequestInformation?.BatchError?.Code == BatchErrorCodeStrings.PoolExists)
+ {
+ Console.WriteLine("The pool {0} already existed when we tried to create it", PoolId);
+ }
+ else
+ {
+ throw; // Any other exception is unexpected
+ }
+ }
+ }
+
+ public static VirtualMachineConfiguration CreateVirtualMachineConfiguration(ImageReference imageReference)
+ {
+ VirtualMachineConfiguration config = new VirtualMachineConfiguration(
+ imageReference: imageReference,
+ nodeAgentSkuId: "batch.node.windows amd64");
+
+ config.DataDisks = new List();
+ config.DataDisks.Add(new DataDisk(0, 2048, CachingType.ReadOnly, StorageAccountType.PremiumLrs));
+
+ return config;
+ }
+
+ public static ImageReference CreateImageReference()
+ {
+ return new ImageReference(
+ publisher: "MicrosoftWindowsServer",
+ offer: "WindowsServer",
+ sku: "2019-datacenter-smalldisk",
+ version: "latest");
+ }
+
+ public static void CreateBatchTasks(string action, string jobId, string containerUrl, string batchAccountUrl, string sqlServerName, string accessToken, dynamic databases, ILogger log)
+ {
+ // Get a Batch client using function identity
+ log.LogInformation("CreateBatchTasks: entering");
+ var azureServiceTokenProvider = new AzureServiceTokenProvider();
+ BatchTokenCredentials batchCred = new BatchTokenCredentials(batchAccountUrl, azureServiceTokenProvider.GetAccessTokenAsync("https://batch.core.windows.net/").Result);
+ using (BatchClient batchClient = BatchClient.Open(batchCred))
+ {
+ // For each database, submit the Exporting job to Azure Batch Compute Pool.
+ log.LogInformation("CreateBatchTasks: enumerating databases");
+ List tasks = new List();
+ foreach (var db in databases)
+ {
+ string serverDatabaseName = db.name.ToString();
+ string logicalDatabase = serverDatabaseName.Remove(0, sqlServerName.Length + 1);
+
+ log.LogInformation("CreateBatchTasks: creating task for database {0}", logicalDatabase);
+ string taskId = sqlServerName + "_" + logicalDatabase;
+ string command = string.Format("cmd /c %AZ_BATCH_APP_PACKAGE_{0}#{1}%\\BatchWrapper {2}", AppPackageName.ToUpper(), AppPackageVersion, action);
+ command += string.Format(" {0} {1} {2} {3} {4}", sqlServerName, logicalDatabase, accessToken, AppPackageName.ToUpper(), AppPackageVersion);
+ string taskCommandLine = string.Format(command);
+
+ CloudTask singleTask = new CloudTask(taskId, taskCommandLine);
+ singleTask.EnvironmentSettings = new[] { new EnvironmentSetting("JOB_CONTAINER_URL", containerUrl) };
+
+ Console.WriteLine(string.Format("Adding task {0} to job ...", taskId));
+ tasks.Add(singleTask);
+ }
+
+ // Add all tasks to the job.
+ batchClient.JobOperations.AddTask(jobId, tasks);
+ }
+ log.LogInformation("CreateBatchTasks: exiting");
+ }
+ }
+}
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl/HttpSurface.cs b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl/HttpSurface.cs
new file mode 100644
index 0000000000..2dcacb9142
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl/HttpSurface.cs
@@ -0,0 +1,103 @@
+using Microsoft.Azure.WebJobs;
+using Microsoft.Azure.WebJobs.Extensions.DurableTask;
+using Microsoft.Azure.WebJobs.Extensions.Http;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+namespace ADPControl
+{
+ public static class HttpSurface
+ {
+ public class ExportRequest
+ {
+ public Guid SubscriptionId { get; set; }
+
+ public string ResourceGroupName { get; set; }
+
+ public string SourceSqlServerResourceGroupName { get; set; }
+
+ public string SourceSqlServerName { get; set; }
+
+ public string BatchAccountUrl { get; set; }
+
+ public string StorageAccountName { get; set; }
+
+ public string AccessToken { get; set; }
+
+ public string VNetSubnetId { get; set; }
+ }
+
+ public class ImportRequest
+ {
+ public Guid SubscriptionId { get; set; }
+
+ public string ResourceGroupName { get; set; }
+
+ public string TargetSqlServerResourceGroupName { get; set; }
+
+ public string TargetSqlServerName { get; set; }
+
+ public string TargetAccessToken { get; set; }
+
+ public string BatchAccountUrl { get; set; }
+
+ public string StorageAccountName { get; set; }
+
+ public string ContainerName { get; set; }
+
+ public string SqlAdminPassword { get; set; }
+
+ public string VNetSubnetId { get; set; }
+ }
+
+ [FunctionName("Export")]
+ public static async Task PostExport(
+ [HttpTrigger(AuthorizationLevel.Function, "post", Route = "subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/Export")]
+ HttpRequestMessage req,
+ [DurableClient] IDurableOrchestrationClient starter,
+ ILogger log,
+ Guid subscriptionId,
+ string resourceGroupName)
+ {
+ log.LogInformation("C# HTTP trigger function processed an Export request.");
+ ExportRequest request = await req.Content.ReadAsAsync();
+
+ request.SubscriptionId = subscriptionId;
+ request.ResourceGroupName = resourceGroupName;
+
+ if (request.SourceSqlServerResourceGroupName == null)
+ request.SourceSqlServerResourceGroupName = resourceGroupName;
+
+ string instanceId = await starter.StartNewAsync(nameof(Orchestrator.RunExportOrchestrator), request);
+
+ log.LogInformation($"Started orchestration with ID = '{instanceId}'.");
+ return starter.CreateCheckStatusResponse(req, instanceId);
+ }
+
+ [FunctionName("Import")]
+ public static async Task PostImport(
+ [HttpTrigger(AuthorizationLevel.Function, "post", Route = "subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/Import")]
+ HttpRequestMessage req,
+ [DurableClient] IDurableOrchestrationClient starter,
+ ILogger log,
+ Guid subscriptionId,
+ string resourceGroupName)
+ {
+ log.LogInformation("C# HTTP trigger function processed an Import request.");
+ ImportRequest request = await req.Content.ReadAsAsync();
+
+ request.SubscriptionId = subscriptionId;
+ request.ResourceGroupName = resourceGroupName;
+
+ if (request.TargetSqlServerResourceGroupName == null)
+ request.TargetSqlServerResourceGroupName = resourceGroupName;
+
+ string instanceId = await starter.StartNewAsync(nameof(Orchestrator.RunImportOrchestrator), request);
+
+ log.LogInformation($"Started orchestration with ID = '{instanceId}'.");
+ return starter.CreateCheckStatusResponse(req, instanceId);
+ }
+ }
+}
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl/Orchestrator.cs b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl/Orchestrator.cs
new file mode 100644
index 0000000000..9794aefba2
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl/Orchestrator.cs
@@ -0,0 +1,104 @@
+using Microsoft.Azure.WebJobs;
+using Microsoft.Azure.WebJobs.Extensions.DurableTask;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using static ADPControl.HttpSurface;
+
+namespace ADPControl
+{
+ public static class Orchestrator
+ {
+ // The Import Orchestrator
+ [FunctionName(nameof(RunImportOrchestrator))]
+ public static async Task RunImportOrchestrator(
+ [OrchestrationTrigger] IDurableOrchestrationContext context, ILogger log)
+ {
+ log.LogInformation("RunImportOrchestrator: entering");
+
+ try {
+
+ ImportRequest importRequest = context.GetInput();
+ // Deploy the ARM template to Create empty SQL resource
+ string deploymentName = await context.CallActivityAsync(nameof(AzureResourceManagerActivity.BeginDeployArmTemplateForImport), importRequest);
+ while (true)
+ {
+ log.LogInformation("RunImportOrchestrator: starting ARM deployment");
+ string status = await context.CallActivityAsync(nameof(AzureResourceManagerActivity.GetArmDeploymentForImport), (importRequest.SubscriptionId, importRequest.TargetSqlServerResourceGroupName, deploymentName));
+ if (status == "Succeeded")
+ {
+ log.LogInformation("RunImportOrchestrator: ARM deployment succeeded");
+ break;
+ }
+ else if (status == "Failed")
+ {
+ log.LogInformation("RunImportOrchestrator: ARM deployment failed");
+ throw new Exception("Failed ARM Deployment");
+ }
+
+ // Orchestration sleeps until this time.
+ var nextCheck = context.CurrentUtcDateTime.AddSeconds(10);
+
+ if (!context.IsReplaying) { log.LogInformation($"RunImportOrchestrator: Replaying ARM deployment, next check at {nextCheck}."); }
+ await context.CreateTimer(nextCheck, CancellationToken.None);
+ }
+
+ log.LogInformation("RunImportOrchestrator: Enumerating databases");
+ var databases = await context.CallActivityAsync(nameof(AzureResourceManagerActivity.GetArmTemplateForImportSkipParameterization), importRequest);
+
+ // Create BatchPool And Job
+ log.LogInformation("RunImportOrchestrator: Creating batch pool and import job");
+ string jobId = await context.CallActivityAsync(nameof(BatchActivity.CreateBatchPoolAndImportJob), importRequest);
+
+ string containerUrl = await context.CallActivityAsync(nameof(StorageActivity.GettingJobContainerUrl), (importRequest.SubscriptionId, importRequest.ResourceGroupName, importRequest.StorageAccountName, importRequest.ContainerName));
+
+ log.LogInformation("RunImportOrchestrator: Creating import database tasks");
+ BatchActivity.CreateBatchTasks("Import", jobId, containerUrl, importRequest.BatchAccountUrl, importRequest.TargetSqlServerName, importRequest.TargetAccessToken, databases, log);
+
+ // create output values
+ Tuple[] outputValues = {
+ Tuple.Create("Orchestration progress:", "Complete"),
+ Tuple.Create("deploymentName", deploymentName),
+ Tuple.Create("jobId", jobId),
+ Tuple.Create("containerUrl", containerUrl)
+ };
+ context.SetOutput(outputValues);
+ }
+ finally {
+ log.LogInformation("RunImportOrchestrator: exiting");
+ }
+ }
+
+ // The Export Orchestrator
+ [FunctionName(nameof(RunExportOrchestrator))]
+ public static async Task RunExportOrchestrator(
+ [OrchestrationTrigger] IDurableOrchestrationContext context, ILogger log)
+ {
+ ExportRequest exportRequest = context.GetInput();
+
+ // Getting the ARM template Skip ResourceName Parameterization.
+ var databases = await context.CallActivityAsync(nameof(AzureResourceManagerActivity.GetArmTemplateForExportSkipParameterization), exportRequest);
+
+ // Getting the ARM template.
+ dynamic Template = await context.CallActivityAsync(nameof(AzureResourceManagerActivity.GetArmTemplateForExport), exportRequest);
+ string json = JsonConvert.SerializeObject(Template);
+
+ // Create BatchPool And Job
+ string jobId = await context.CallActivityAsync(nameof(BatchActivity.CreateBatchPoolAndExportJob), exportRequest);
+
+ string containerUrl = await context.CallActivityAsync(nameof(StorageActivity.GettingJobContainerUrl), (exportRequest.SubscriptionId, exportRequest.ResourceGroupName, exportRequest.StorageAccountName, jobId));
+ await context.CallActivityAsync(nameof(StorageActivity.UploadingArmTemplate), (containerUrl, json));
+
+ BatchActivity.CreateBatchTasks("Export", jobId, containerUrl, exportRequest.BatchAccountUrl, exportRequest.SourceSqlServerName, exportRequest.AccessToken, databases, log);
+
+ // create output values
+ Tuple[] outputValues = {
+ Tuple.Create("jobId", jobId),
+ Tuple.Create("containerUrl", containerUrl)
+ };
+ context.SetOutput(outputValues);
+ }
+ }
+}
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl/StorageActivity.cs b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl/StorageActivity.cs
new file mode 100644
index 0000000000..d56791621c
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl/StorageActivity.cs
@@ -0,0 +1,79 @@
+using Microsoft.Azure.Management.Storage;
+using Microsoft.Azure.Management.Storage.Models;
+using Microsoft.Azure.Services.AppAuthentication;
+using Microsoft.Azure.WebJobs;
+using Microsoft.Azure.WebJobs.Extensions.DurableTask;
+using Microsoft.Extensions.Logging;
+using Microsoft.Rest;
+using Microsoft.WindowsAzure.Storage;
+using Microsoft.WindowsAzure.Storage.Auth;
+using Microsoft.WindowsAzure.Storage.Blob;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace ADPControl
+{
+ public static class StorageActivity
+ {
+ private const string ArmTemplateFileName = "template.json";
+
+ [FunctionName(nameof(GettingJobContainerUrl))]
+ public static string GettingJobContainerUrl([ActivityTrigger] (Guid, string, string, string) input, ILogger log)
+ {
+ Guid SubscriptionId = input.Item1;
+ String ResourceGroupName = input.Item2;
+ String StorageAccountName = input.Item3;
+ String ContainerName = input.Item4;
+
+ var azureServiceTokenProvider = new AzureServiceTokenProvider();
+ TokenCredentials tokenArmCredential = new TokenCredentials(azureServiceTokenProvider.GetAccessTokenAsync("https://management.core.windows.net/").Result);
+ StorageManagementClient storageMgmtClient = new StorageManagementClient(tokenArmCredential) { SubscriptionId = SubscriptionId.ToString() };
+
+ // Get the storage account keys for a given account and resource group
+ IList acctKeys = storageMgmtClient.StorageAccounts.ListKeys(ResourceGroupName, StorageAccountName).Keys;
+
+ // Get a Storage account using account creds:
+ StorageCredentials storageCred = new StorageCredentials(StorageAccountName, acctKeys.FirstOrDefault().Value);
+ CloudStorageAccount linkedStorageAccount = new CloudStorageAccount(storageCred, true);
+
+ bool createContainer = false;
+ // Normalize the container name for the Export action.
+ if (ContainerName.Contains("-Export-"))
+ {
+ ContainerName = ContainerName.Replace("Export-", "");
+ createContainer = true;
+ }
+
+ CloudBlobContainer container = linkedStorageAccount.CreateCloudBlobClient().GetContainerReference(ContainerName);
+
+ if(createContainer)
+ container.CreateIfNotExistsAsync().Wait();
+
+ string containerUrl = container.Uri.ToString() +
+ container.GetSharedAccessSignature(new SharedAccessBlobPolicy()
+ {
+ Permissions = SharedAccessBlobPermissions.Write | SharedAccessBlobPermissions.Read | SharedAccessBlobPermissions.List,
+ SharedAccessExpiryTime = DateTime.UtcNow.AddDays(7)
+ });
+ return containerUrl;
+ }
+
+ [FunctionName(nameof(UploadingArmTemplate))]
+ public static void UploadingArmTemplate([ActivityTrigger] (string, string) input, ILogger log)
+ {
+ string containerUrl = input.Item1;
+ string json = input.Item2;
+
+ CloudBlobContainer container = new CloudBlobContainer(new Uri(containerUrl));
+ CloudBlockBlob blob = container.GetBlockBlobReference(ArmTemplateFileName);
+ blob.Properties.ContentType = "application/json";
+ using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(json)))
+ {
+ blob.UploadFromStreamAsync(stream).Wait();
+ }
+ }
+ }
+}
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl/host.json b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl/host.json
new file mode 100644
index 0000000000..ee986a759d
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/ADPControl/host.json
@@ -0,0 +1,16 @@
+{
+ "version": "2.0",
+ "extensions": {
+ "durableTask": {
+ "hubName": "adp"
+ }
+ },
+ "logging": {
+ "applicationInsights": {
+ "samplingExcludedTypes": "Request",
+ "samplingSettings": {
+ "isEnabled": true
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/BatchWrapper/ActionType.cs b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/BatchWrapper/ActionType.cs
new file mode 100644
index 0000000000..28602c922e
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/BatchWrapper/ActionType.cs
@@ -0,0 +1,12 @@
+namespace BatchWrapper
+{
+ ///
+ /// The type of sqlpackage action to perform.
+ ///
+ public enum ActionType
+ {
+ DefaultInvalid = -1,
+ Export,
+ Import
+ }
+}
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/BatchWrapper/App.config b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/BatchWrapper/App.config
new file mode 100644
index 0000000000..3617b3e032
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/BatchWrapper/App.config
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/BatchWrapper/BatchWrapper.csproj b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/BatchWrapper/BatchWrapper.csproj
new file mode 100644
index 0000000000..bbbcf923de
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/BatchWrapper/BatchWrapper.csproj
@@ -0,0 +1,98 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {E64CD92E-2D2C-48F1-B6B1-A7AF819B06F1}
+ Exe
+ BatchWrapper
+ BatchWrapper
+ v4.7.2
+ 8.0
+ 512
+ true
+ true
+ false
+ publish\
+ true
+ Disk
+ false
+ Foreground
+ 7
+ Days
+ false
+ false
+ true
+ 0
+ 1.0.0.%2a
+ false
+ true
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ ..\packages\Microsoft.Azure.Batch.Conventions.Files.3.5.1\lib\net461\Microsoft.Azure.Batch.Conventions.Files.dll
+
+
+ ..\packages\WindowsAzure.Storage.9.3.3\lib\net45\Microsoft.WindowsAzure.Storage.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ False
+ Microsoft .NET Framework 4.7.2 %28x86 and x64%29
+ true
+
+
+ False
+ .NET Framework 3.5 SP1
+ false
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/BatchWrapper/Constants.cs b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/BatchWrapper/Constants.cs
new file mode 100644
index 0000000000..ded7e29970
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/BatchWrapper/Constants.cs
@@ -0,0 +1,34 @@
+namespace BatchWrapper
+{
+ ///
+ /// Constants for the batch wrapper.
+ ///
+ public static class Constants
+ {
+ ///
+ /// Environment variable names present or needed during the batch task execution.
+ ///
+ public static class EnvironmentVariableNames
+ {
+ ///
+ /// Path to the directory containing the sqlpackage exe.
+ ///
+ internal const string AppPackagePrefix = "AZ_BATCH_APP_PACKAGE";
+
+ ///
+ /// Path to the working directory assigned to the batch task.
+ ///
+ internal const string TaskWorkingDir = "AZ_BATCH_TASK_WORKING_DIR";
+
+ ///
+ /// Path to the working directory assigned to the batch task.
+ ///
+ internal const string AzBatchTaskId = "AZ_BATCH_TASK_ID";
+
+ ///
+ /// Path to the working directory assigned to the batch task.
+ ///
+ internal const string JobContainerUrl = "JOB_CONTAINER_URL";
+ }
+ }
+}
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/BatchWrapper/Payload.cs b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/BatchWrapper/Payload.cs
new file mode 100644
index 0000000000..2a037d8511
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/BatchWrapper/Payload.cs
@@ -0,0 +1,38 @@
+namespace BatchWrapper
+{
+ ///
+ /// The top-level object stored in the key vault for an import/export operation.
+ ///
+ public sealed class Payload
+ {
+ ///
+ /// The Name of sqlpackage to use for performing the import/export operation.
+ ///
+ public string ApplicatonPackageName{ get; set; }
+
+ ///
+ /// The Version of sqlpackage to use for performing the import/export operation.
+ ///
+ public string ApplicatonPackageVersion { get; set; }
+
+ ///
+ /// The type of sqlpackage action to perform.
+ ///
+ public ActionType Action { get; set; }
+
+ ///
+ /// The logical server name to export from or import to.
+ ///
+ public string LogicalServerName { get; set; }
+
+ ///
+ /// The database name to export from or import to.
+ ///
+ public string DatabaseName { get; set; }
+
+ ///
+ /// The server admin username.
+ ///
+ public string AccessToken { get; set; }
+ }
+}
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/BatchWrapper/Program.cs b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/BatchWrapper/Program.cs
new file mode 100644
index 0000000000..72237b1a8d
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/BatchWrapper/Program.cs
@@ -0,0 +1,178 @@
+using Microsoft.Azure.Batch.Conventions.Files;
+using Microsoft.WindowsAzure.Storage.Blob;
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace BatchWrapper
+{
+ public static class Program
+ {
+ private static string dataDirectory = "F:\\data";
+ private static string tempDirectory = "F:\\temp";
+ private static string[] directories = { dataDirectory, tempDirectory };
+
+ private static readonly TimeSpan stdoutFlushDelay = TimeSpan.FromSeconds(3);
+
+ private static void WriteLine(string message) => WriteLineInternal(Console.Out, message);
+ private static void WriteErrorLine(string message) => WriteLineInternal(Console.Error, message);
+ private static void WriteLineInternal(TextWriter writer, string message)
+ {
+ var lines = message?.Split('\n') ?? new string[0];
+ foreach (var line in lines)
+ {
+ writer.WriteLine($"[{DateTime.UtcNow:u}] {line?.TrimEnd()}");
+ }
+ }
+
+ public static async Task Main(string[] args)
+ {
+ var assembly = typeof(Program).Assembly;
+ WriteLine($"{assembly.ManifestModule.Name} v{assembly.GetName().Version.ToString(3)}");
+
+ // Get the command payload
+ var payload = new Payload();
+
+ if (args.Length > 0)
+ {
+ payload.Action = (ActionType)Enum.Parse(typeof(ActionType), args[0]);
+ payload.LogicalServerName = args[1] + ".database.windows.net";
+ payload.DatabaseName = args[2];
+ payload.AccessToken = args[3];
+ payload.ApplicatonPackageName = args[4];
+ payload.ApplicatonPackageVersion = args[5];
+ }
+
+ // Cleanup folders
+ foreach (string dir in directories)
+ {
+ if (Directory.Exists(dir))
+ {
+ Directory.Delete(dir, true);
+ }
+
+ Directory.CreateDirectory(dir);
+ }
+
+ string sqlPackageBacpacFile = Path.Combine(dataDirectory, payload.DatabaseName + ".bacpac");
+ string sqlPackageLogPath = payload.DatabaseName + ".log";
+
+ var targetDir = Environment.GetEnvironmentVariable(Constants.EnvironmentVariableNames.AppPackagePrefix + "_" + payload.ApplicatonPackageName + "#" + payload.ApplicatonPackageVersion);
+ var workingDir = Environment.GetEnvironmentVariable(Constants.EnvironmentVariableNames.TaskWorkingDir);
+
+ string taskId = Environment.GetEnvironmentVariable(Constants.EnvironmentVariableNames.AzBatchTaskId);
+ string jobContainerUrl = Environment.GetEnvironmentVariable(Constants.EnvironmentVariableNames.JobContainerUrl);
+
+ // Build the import/export command
+ var cmdBuilder = new StringBuilder();
+ cmdBuilder.Append($"/Action:{payload.Action}");
+ cmdBuilder.Append(" /MaxParallelism:16");
+ cmdBuilder.Append(String.Format(" /DiagnosticsFile:{0}", sqlPackageLogPath));
+ cmdBuilder.Append(" /p:CommandTimeout=604800");
+
+ switch (payload.Action)
+ {
+ case ActionType.Export:
+ cmdBuilder.Append($" /SourceServerName:{payload.LogicalServerName}");
+ cmdBuilder.Append($" /SourceDatabaseName:{payload.DatabaseName}");
+ cmdBuilder.Append($" /AccessToken:{payload.AccessToken}");
+ cmdBuilder.Append($" /TargetFile:{sqlPackageBacpacFile}");
+ cmdBuilder.Append($" /SourceTimeout:30");
+ cmdBuilder.Append(String.Format(" /p:TempDirectoryForTableData=\"{0}\"", tempDirectory));
+ cmdBuilder.Append(" /p:VerifyFullTextDocumentTypesSupported=false");
+ break;
+
+ case ActionType.Import:
+ cmdBuilder.Append($" /TargetServerName:{payload.LogicalServerName}");
+ cmdBuilder.Append($" /TargetDatabaseName:{payload.DatabaseName}");
+ cmdBuilder.Append($" /AccessToken:{payload.AccessToken}");
+ cmdBuilder.Append($" /TargetTimeout:30");
+ cmdBuilder.Append($" /SourceFile:{sqlPackageBacpacFile}");
+ break;
+
+ default:
+ throw new ArgumentException($"Invalid action type: {payload.Action}");
+ }
+
+ if (payload.Action == ActionType.Import)
+ {
+ WriteLine(string.Format("Downloading {0} bacpac file to {1}", payload.DatabaseName, sqlPackageBacpacFile));
+ CloudBlobContainer container = new CloudBlobContainer(new Uri(jobContainerUrl));
+ CloudBlockBlob blob = container.GetBlockBlobReference(String.Format("$JobOutput/{0}.bacpac", payload.DatabaseName));
+ blob.DownloadToFile(sqlPackageBacpacFile, FileMode.CreateNew);
+
+ if (File.Exists(sqlPackageBacpacFile))
+ {
+ WriteLine(string.Format("Downloaded {0} bacpac file to {1}", payload.DatabaseName, sqlPackageBacpacFile));
+ }
+ else
+ {
+ throw new Exception(string.Format("{0} didn't download", sqlPackageBacpacFile));
+ }
+ }
+
+ // Perform the import/export process
+ var startTime = DateTimeOffset.UtcNow;
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ WorkingDirectory = workingDir,
+ FileName = Path.Combine(targetDir, "sqlpackage.exe"),
+ Arguments = cmdBuilder.ToString(),
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true
+ }
+ };
+
+ process.OutputDataReceived += (s, e) => WriteLine(e.Data);
+ process.ErrorDataReceived += (s, e) => WriteErrorLine(e.Data);
+ process.Start();
+ process.BeginOutputReadLine();
+ process.BeginErrorReadLine();
+ process.WaitForExit();
+
+ WriteLine(String.Format("SqlPackage.exe exited with code: {0}", process.ExitCode));
+
+ if (payload.Action == ActionType.Export)
+ {
+ if (File.Exists(sqlPackageBacpacFile))
+ {
+ WriteLine(string.Format("Downloaded {0} bacpac file to {1}", payload.DatabaseName, sqlPackageBacpacFile));
+ }
+ else
+ {
+ throw new Exception(string.Format("{0} didn't downloaded", sqlPackageBacpacFile));
+ }
+
+ // Persist the Job Output
+ JobOutputStorage jobOutputStorage = new JobOutputStorage(new Uri(jobContainerUrl));
+
+ await jobOutputStorage.SaveAsync(JobOutputKind.JobOutput, sqlPackageLogPath);
+ WriteLine(String.Format("Uploaded {0} to job account", sqlPackageLogPath));
+
+ await jobOutputStorage.SaveAsync(JobOutputKind.JobOutput, sqlPackageBacpacFile, payload.DatabaseName + ".bacpac");
+ WriteLine(String.Format("Uploaded {0} to job account", sqlPackageBacpacFile));
+ }
+
+ // We are tracking the disk file to save our standard output, but the node agent may take
+ // up to 3 seconds to flush the stdout stream to disk. So give the file a moment to catch up.
+ await Task.Delay(stdoutFlushDelay);
+
+ // Cleanup folders
+ foreach (string dir in directories)
+ {
+ if (Directory.Exists(dir))
+ {
+ Directory.Delete(dir, true);
+ }
+ }
+
+ return process.ExitCode;
+ }
+ }
+}
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/BatchWrapper/packages.config b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/BatchWrapper/packages.config
new file mode 100644
index 0000000000..f8121f7edd
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/BatchWrapper/packages.config
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/SqlPackageWrapper/.vscode/launch.json b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/SqlPackageWrapper/.vscode/launch.json
new file mode 100644
index 0000000000..9035a71e76
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/SqlPackageWrapper/.vscode/launch.json
@@ -0,0 +1,27 @@
+{
+ // Use IntelliSense to find out which attributes exist for C# debugging
+ // Use hover for the description of the existing attributes
+ // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": ".NET Core Launch (console)",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "build",
+ // If you have changed target frameworks, make sure to update the program path.
+ "program": "${workspaceFolder}/bin/Debug/netcoreapp3.1/SqlPackageWrapper.dll",
+ "args": [],
+ "cwd": "${workspaceFolder}",
+ // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
+ "console": "internalConsole",
+ "stopAtEntry": false
+ },
+ {
+ "name": ".NET Core Attach",
+ "type": "coreclr",
+ "request": "attach",
+ "processId": "${command:pickProcess}"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/SqlPackageWrapper/.vscode/tasks.json b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/SqlPackageWrapper/.vscode/tasks.json
new file mode 100644
index 0000000000..a6efb6b11c
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/SqlPackageWrapper/.vscode/tasks.json
@@ -0,0 +1,42 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "label": "build",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "build",
+ "${workspaceFolder}/SqlPackageWrapper.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary"
+ ],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "publish",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "publish",
+ "${workspaceFolder}/SqlPackageWrapper.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary"
+ ],
+ "problemMatcher": "$msCompile"
+ },
+ {
+ "label": "watch",
+ "command": "dotnet",
+ "type": "process",
+ "args": [
+ "watch",
+ "run",
+ "${workspaceFolder}/SqlPackageWrapper.csproj",
+ "/property:GenerateFullPaths=true",
+ "/consoleloggerparameters:NoSummary"
+ ],
+ "problemMatcher": "$msCompile"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/SqlPackageWrapper/ActionType.cs b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/SqlPackageWrapper/ActionType.cs
new file mode 100644
index 0000000000..279ce0f71c
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/SqlPackageWrapper/ActionType.cs
@@ -0,0 +1,12 @@
+namespace SqlPackageWrapper
+{
+ ///
+ /// The type of sqlpackage action to perform.
+ ///
+ public enum ActionType
+ {
+ DefaultInvalid = -1,
+ Export,
+ Import
+ }
+}
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/SqlPackageWrapper/Constants.cs b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/SqlPackageWrapper/Constants.cs
new file mode 100644
index 0000000000..e94bc9b1c9
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/SqlPackageWrapper/Constants.cs
@@ -0,0 +1,39 @@
+namespace SqlPackageWrapper
+{
+ ///
+ /// Constants for the batch wrapper.
+ ///
+ public static class Constants
+ {
+ ///
+ /// Environment variable names present or needed during the batch task execution.
+ ///
+ public static class EnvironmentVariableNames
+ {
+ ///
+ /// Path to the directory containing the batch wrapper exe.
+ ///
+ public const string WrapperLocation = "AZ_BATCH_APP_PACKAGE_BATCHWRAPPER";
+
+ ///
+ /// Path to the directory containing the sqlpackage exe.
+ ///
+ internal const string SqlPackageLocation = "AZ_BATCH_APP_PACKAGE_SQLPACKAGE";
+
+ ///
+ /// Path to the working directory assigned to the batch task.
+ ///
+ internal const string TaskWorkingDir = "AZ_BATCH_TASK_WORKING_DIR";
+
+ ///
+ /// Path to the working directory assigned to the batch task.
+ ///
+ internal const string AzBatchTaskId = "AZ_BATCH_TASK_ID";
+
+ ///
+ /// Path to the working directory assigned to the batch task.
+ ///
+ internal const string JobContainerUrl = "JOB_CONTAINER_URL";
+ }
+ }
+}
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/SqlPackageWrapper/Payload.cs b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/SqlPackageWrapper/Payload.cs
new file mode 100644
index 0000000000..170ec9d515
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/SqlPackageWrapper/Payload.cs
@@ -0,0 +1,38 @@
+namespace SqlPackageWrapper
+{
+ ///
+ /// The top-level object stored in the key vault for an import/export operation.
+ ///
+ public sealed class Payload
+ {
+ ///
+ /// The version of sqlpackage to use for performing the import/export operation.
+ ///
+ public string SqlPackageVersion { get; set; }
+
+ ///
+ /// The type of sqlpackage action to perform.
+ ///
+ public ActionType Action { get; set; }
+
+ ///
+ /// The logical server name to export from or import to.
+ ///
+ public string LogicalServerName { get; set; }
+
+ ///
+ /// The database name to export from or import to.
+ ///
+ public string DatabaseName { get; set; }
+
+ ///
+ /// The server admin username.
+ ///
+ public string Username { get; set; }
+
+ ///
+ /// The server admin password.
+ ///
+ public string Password { get; set; }
+ }
+}
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/SqlPackageWrapper/Program.cs b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/SqlPackageWrapper/Program.cs
new file mode 100644
index 0000000000..2ee94c255f
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/SqlPackageWrapper/Program.cs
@@ -0,0 +1,162 @@
+using Microsoft.Azure.Batch.Conventions.Files;
+using Microsoft.WindowsAzure.Storage.Blob;
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SqlPackageWrapper
+{
+ public static class Program
+ {
+ private static string dataDirectory = "F:\\data";
+ private static string tempDirectory = "F:\\temp";
+ private static string[] directories = { dataDirectory, tempDirectory };
+
+ private static readonly TimeSpan stdoutFlushDelay = TimeSpan.FromSeconds(3);
+
+ private static void WriteLine(string message) => WriteLineInternal(Console.Out, message);
+ private static void WriteErrorLine(string message) => WriteLineInternal(Console.Error, message);
+ private static void WriteLineInternal(TextWriter writer, string message)
+ {
+ var lines = message?.Split('\n') ?? new string[0];
+ foreach (var line in lines)
+ {
+ writer.WriteLine($"[{DateTime.UtcNow:u}] {line?.TrimEnd()}");
+ }
+ }
+
+ public static async Task Main(string[] args)
+ {
+ var assembly = typeof(Program).Assembly;
+ WriteLine($"{assembly.ManifestModule.Name} v{assembly.GetName().Version.ToString(3)}");
+
+ // Get the command payload
+ var payload = new Payload();
+
+ if (args.Length > 0)
+ {
+ payload.Action = (ActionType)Enum.Parse(typeof(ActionType), args[0]);
+ payload.LogicalServerName = args[1] + ".database.windows.net";
+ payload.DatabaseName = args[2];
+ payload.Username = args[3];
+ payload.Password = args[4];
+ payload.SqlPackageVersion = args[5];
+ }
+
+ // Cleanup folders
+ foreach (string dir in directories)
+ {
+ if (Directory.Exists(dir))
+ {
+ Directory.Delete(dir, true);
+ }
+
+ Directory.CreateDirectory(dir);
+ }
+
+ string sqlPackageDataPath = Path.Combine(dataDirectory, payload.DatabaseName + ".bacpac");
+ string sqlPackageLogPath = Path.Combine(dataDirectory, payload.DatabaseName + ".log");
+
+ var targetDir = Environment.GetEnvironmentVariable($"{Constants.EnvironmentVariableNames.SqlPackageLocation}#{payload.SqlPackageVersion}");
+ var workingDir = Environment.GetEnvironmentVariable(Constants.EnvironmentVariableNames.TaskWorkingDir);
+
+ string taskId = Environment.GetEnvironmentVariable(Constants.EnvironmentVariableNames.AzBatchTaskId);
+ string jobContainerUrl = Environment.GetEnvironmentVariable(Constants.EnvironmentVariableNames.JobContainerUrl);
+
+ // Build the import/export command
+ var cmdBuilder = new StringBuilder();
+ cmdBuilder.Append($"/Action:{payload.Action}");
+ cmdBuilder.Append(" /MaxParallelism:16");
+ cmdBuilder.Append(String.Format(" /DiagnosticsFile:{0}", sqlPackageLogPath));
+ cmdBuilder.Append(" /p:CommandTimeout=86400");
+
+ switch (payload.Action)
+ {
+ case ActionType.Export:
+ cmdBuilder.Append($" /SourceServerName:{payload.LogicalServerName}");
+ cmdBuilder.Append($" /SourceDatabaseName:{payload.DatabaseName}");
+ cmdBuilder.Append($" /SourceUser:{payload.Username}");
+ cmdBuilder.Append($" /SourcePassword:{payload.Password}");
+ cmdBuilder.Append($" /TargetFile:{sqlPackageDataPath}");
+ cmdBuilder.Append(String.Format(" /p:TempDirectoryForTableData=\"{0}\"", tempDirectory));
+ cmdBuilder.Append(" /p:VerifyFullTextDocumentTypesSupported=false");
+ break;
+
+ case ActionType.Import:
+ cmdBuilder.Append($" /TargetServerName:{payload.LogicalServerName}");
+ cmdBuilder.Append($" /TargetDatabaseName:{payload.DatabaseName}");
+ cmdBuilder.Append($" /TargetUser:{payload.Username}");
+ cmdBuilder.Append($" /TargetPassword:{payload.Password}");
+ cmdBuilder.Append($" /SourceFile:{sqlPackageDataPath}");
+ break;
+
+ default:
+ throw new ArgumentException($"Invalid action type: {payload.Action}");
+ }
+
+ if (payload.Action == ActionType.Import)
+ {
+ WriteLine(string.Format("Downloading {0} bacpac file to {1}", payload.DatabaseName, sqlPackageDataPath));
+ CloudBlobContainer container = new CloudBlobContainer(new Uri(jobContainerUrl));
+ CloudBlockBlob blob = container.GetBlockBlobReference(String.Format("$JobOutput/{0}.bacpac", payload.DatabaseName));
+ await blob.DownloadToFileAsync(sqlPackageDataPath, FileMode.CreateNew);
+ WriteLine(string.Format("Downloaded {0} bacpac file to {1}", payload.DatabaseName, sqlPackageDataPath));
+
+ await Task.Delay(stdoutFlushDelay);
+ }
+
+ // Perform the import/export process
+ var startTime = DateTimeOffset.UtcNow;
+ var process = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ WorkingDirectory = workingDir,
+ FileName = Path.Combine(targetDir, "sqlpackage.exe"),
+ Arguments = cmdBuilder.ToString(),
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true
+ }
+ };
+ process.OutputDataReceived += (s, e) => WriteLine(e.Data);
+ process.ErrorDataReceived += (s, e) => WriteErrorLine(e.Data);
+ process.Start();
+ process.BeginOutputReadLine();
+ process.BeginErrorReadLine();
+ process.WaitForExit();
+
+ WriteLine(String.Format("SqlPackage.exe exited with code: {0}", process.ExitCode));
+
+ if (payload.Action == ActionType.Export)
+ {
+ // Persist the Job Output
+ JobOutputStorage jobOutputStorage = new JobOutputStorage(new Uri(jobContainerUrl));
+
+ await jobOutputStorage.SaveAsync(JobOutputKind.JobOutput, sqlPackageLogPath, payload.DatabaseName + ".log");
+ WriteLine(String.Format("Uploaded {0} to job account", sqlPackageLogPath));
+
+ await jobOutputStorage.SaveAsync(JobOutputKind.JobOutput, sqlPackageDataPath, payload.DatabaseName + ".bacpac");
+ WriteLine(String.Format("Uploaded {0} to job account", sqlPackageDataPath));
+
+ // We are tracking the disk file to save our standard output, but the node agent may take
+ // up to 3 seconds to flush the stdout stream to disk. So give the file a moment to catch up.
+ await Task.Delay(stdoutFlushDelay);
+ }
+
+ // Cleanup folders
+ foreach (string dir in directories)
+ {
+ if (Directory.Exists(dir))
+ {
+ Directory.Delete(dir, true);
+ }
+ }
+
+ return process.ExitCode;
+ }
+ }
+}
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/SqlPackageWrapper/SqlPackageWrapper.csproj b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/SqlPackageWrapper/SqlPackageWrapper.csproj
new file mode 100644
index 0000000000..c7d2d2657e
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/ADP/SqlPackageWrapper/SqlPackageWrapper.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ netcoreapp2.1;net452
+
+
+
+
+
+
+
+
+
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/readme.md b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/readme.md
new file mode 100644
index 0000000000..87fe8db9a0
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/Components/readme.md
@@ -0,0 +1 @@
+Folder for helper components used by notebooks in the Hybrid Cloud Toolkit
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/_config.yml b/extensions/azurehybridtoolkit/notebooks/hybridbook/_config.yml
new file mode 100644
index 0000000000..39f1cbac0a
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/_config.yml
@@ -0,0 +1,2 @@
+title: Azure SQL Hybrid Cloud Toolkit
+description: A collection of notebooks to help deploy, migrate and manage SQL instances and databases in a Hybrid Cloud environment.
\ No newline at end of file
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/_data/toc.yml b/extensions/azurehybridtoolkit/notebooks/hybridbook/_data/toc.yml
new file mode 100644
index 0000000000..d9fc266c4b
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/_data/toc.yml
@@ -0,0 +1,80 @@
+- title: Welcome
+ url: /readme
+ not_numbered: true
+- title: Prerequisites and Initial Setup
+ url: /prereqs
+ not_numbered: true
+
+- title: Search
+ search: true
+
+- title: Networking
+ url: /networking/readme
+ not_numbered: true
+ expand_sections: false
+ sections:
+ - title: Download VPN Client Certificate
+ url: networking/download-VpnClient
+ - title: Create Point-to-Site VPN
+ url: networking/p2svnet-creation
+ - title: Create Site-to-Site VPN
+ url: networking/s2svnet-creation
+- title: Assessments
+ url: /Assessments/readme
+ not_numbered: true
+ expand_sections: false
+ sections:
+ - title: SQL Server Best Practices Assessment
+ url: Assessments/sql-server-assessment
+ - title: Compatibility Assessment
+ url: Assessments/compatibility-assessment
+- title: Provisioning
+ url: /provisioning/readme
+ not_numbered: true
+ expand_sections: false
+ sections:
+ - title: Create Azure SQL Virtual Machine
+ url: provisioning/create-sqlvm
+ - title: Create Azure SQL Managed Instance
+ url: provisioning/create-sqlmi
+ - title: Create Azure SQL Database
+ url: provisioning/create-sqldb
+- title: Data Portability
+ url: /data-portability/readme
+ not_numbered: true
+ expand_sections: false
+ sections:
+ - title: Setup Data Portability
+ url: data-portability/setup-adp
+ - title: Export Azure SQL Server
+ url: data-portability/export-sql-server
+ - title: Import Azure SQL Server
+ url: data-portability/import-sql-server
+- title: High Availability and Disaster Recovery
+ url: /hadr/readme
+ not_numbered: true
+ expand_sections: false
+ sections:
+ - title: Backup Database to Blob Storage
+ url: hadr/backup-to-blob
+ - title: Add Azure Passive Secondary Replica
+ url: hadr/add-passive-secondary
+- title: Offline Migration
+ url: /offline-migration/readme
+ not_numbered: true
+ expand_sections: false
+ sections:
+ - title: Migrate Instance to Azure SQL VM
+ url: offline-migration/instance-to-VM
+ - title: Migrate Database to Azure SQL VM
+ url: offline-migration/db-to-VM
+ - title: Migrate Instance to Azure SQL MI
+ url: offline-migration/instance-to-MI
+ - title: Migrate Database to Azure SQL MI
+ url: offline-migration/db-to-MI
+ - title: Migrate Database to Azure SQL DB
+ url: offline-migration/db-to-SQLDB
+- title: Glossary
+ url: /glossary
+- title: Appendices
+ url: /appendices
\ No newline at end of file
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/css/styles.scss b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/css/styles.scss
new file mode 100644
index 0000000000..da50df3a6c
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/css/styles.scss
@@ -0,0 +1,4 @@
+---
+---
+
+@import 'main';
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/custom/custom.css b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/custom/custom.css
new file mode 100644
index 0000000000..30948f979d
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/custom/custom.css
@@ -0,0 +1,4 @@
+/* Put your custom CSS here */
+.left {
+ margin-left: 0px;
+}
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/custom/custom.js b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/custom/custom.js
new file mode 100644
index 0000000000..792c873992
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/custom/custom.js
@@ -0,0 +1 @@
+// Put your custom javascript here
\ No newline at end of file
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/html/index.html b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/html/index.html
new file mode 100644
index 0000000000..82af59f96e
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/html/index.html
@@ -0,0 +1,25 @@
+---
+permalink: /index.html
+title: "Index"
+layout: none
+---
+
+
+{% for chapter in site.data.toc %}
+{% unless chapter.external %}
+ {% comment %}This ensures that the first link we re-direct to isn't an external site {% endcomment %}
+ {% assign redirectURL = chapter.url | relative_url %}
+ {% break %}
+{% endunless %}
+{% endfor %}
+
+
+
+ Redirecting…
+
+
+
+
+
Redirecting…
+ Click here if you are not redirected.
+
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/html/search_form.html b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/html/search_form.html
new file mode 100644
index 0000000000..4025feb29f
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/html/search_form.html
@@ -0,0 +1,15 @@
+---
+permalink: /search
+title: "Search the site"
+search_page: true
+---
+
+
+
+
+
+
+
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/images/copy-button.svg b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/images/copy-button.svg
new file mode 100644
index 0000000000..7cefcfff69
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/images/copy-button.svg
@@ -0,0 +1 @@
+
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/images/edit-button.svg b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/images/edit-button.svg
new file mode 100644
index 0000000000..8128691bfa
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/images/edit-button.svg
@@ -0,0 +1,81 @@
+
+
+
+
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/images/logo_binder.svg b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/images/logo_binder.svg
new file mode 100644
index 0000000000..45fecf7511
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/images/logo_binder.svg
@@ -0,0 +1,19 @@
+
+
+
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/images/logo_jupyterhub.svg b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/images/logo_jupyterhub.svg
new file mode 100644
index 0000000000..c584badf0b
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/images/logo_jupyterhub.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/images/sqlserver.png b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/images/sqlserver.png
new file mode 100644
index 0000000000..cb0062e2ad
Binary files /dev/null and b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/images/sqlserver.png differ
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/js/anchor.min.js b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/js/anchor.min.js
new file mode 100644
index 0000000000..e302d89b5a
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/js/anchor.min.js
@@ -0,0 +1,9 @@
+// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt Expat
+//
+// AnchorJS - v4.2.0 - 2019-01-01
+// https://github.com/bryanbraun/anchorjs
+// Copyright (c) 2019 Bryan Braun; Licensed MIT
+//
+// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt Expat
+!function(A,e){"use strict";"function"==typeof define&&define.amd?define([],e):"object"==typeof module&&module.exports?module.exports=e():(A.AnchorJS=e(),A.anchors=new A.AnchorJS)}(this,function(){"use strict";return function(A){function f(A){A.icon=A.hasOwnProperty("icon")?A.icon:"",A.visible=A.hasOwnProperty("visible")?A.visible:"hover",A.placement=A.hasOwnProperty("placement")?A.placement:"right",A.ariaLabel=A.hasOwnProperty("ariaLabel")?A.ariaLabel:"Anchor",A.class=A.hasOwnProperty("class")?A.class:"",A.base=A.hasOwnProperty("base")?A.base:"",A.truncate=A.hasOwnProperty("truncate")?Math.floor(A.truncate):64,A.titleText=A.hasOwnProperty("titleText")?A.titleText:""}function p(A){var e;if("string"==typeof A||A instanceof String)e=[].slice.call(document.querySelectorAll(A));else{if(!(Array.isArray(A)||A instanceof NodeList))throw new Error("The selector provided to AnchorJS was invalid.");e=[].slice.call(A)}return e}this.options=A||{},this.elements=[],f(this.options),this.isTouchDevice=function(){return!!("ontouchstart"in window||window.DocumentTouch&&document instanceof DocumentTouch)},this.add=function(A){var e,t,i,n,o,s,a,r,c,h,l,u,d=[];if(f(this.options),"touch"===(l=this.options.visible)&&(l=this.isTouchDevice()?"always":"hover"),A||(A="h2, h3, h4, h5, h6"),0===(e=p(A)).length)return this;for(function(){if(null===document.head.querySelector("style.anchorjs")){var A,e=document.createElement("style");e.className="anchorjs",e.appendChild(document.createTextNode("")),void 0===(A=document.head.querySelector('[rel="stylesheet"], style'))?document.head.appendChild(e):document.head.insertBefore(e,A),e.sheet.insertRule(" .anchorjs-link { opacity: 0; text-decoration: none; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }",e.sheet.cssRules.length),e.sheet.insertRule(" *:hover > .anchorjs-link, .anchorjs-link:focus { opacity: 1; }",e.sheet.cssRules.length),e.sheet.insertRule(" [data-anchorjs-icon]::after { content: attr(data-anchorjs-icon); }",e.sheet.cssRules.length),e.sheet.insertRule(' @font-face { font-family: "anchorjs-icons"; src: url(data:n/a;base64,AAEAAAALAIAAAwAwT1MvMg8yG2cAAAE4AAAAYGNtYXDp3gC3AAABpAAAAExnYXNwAAAAEAAAA9wAAAAIZ2x5ZlQCcfwAAAH4AAABCGhlYWQHFvHyAAAAvAAAADZoaGVhBnACFwAAAPQAAAAkaG10eASAADEAAAGYAAAADGxvY2EACACEAAAB8AAAAAhtYXhwAAYAVwAAARgAAAAgbmFtZQGOH9cAAAMAAAAAunBvc3QAAwAAAAADvAAAACAAAQAAAAEAAHzE2p9fDzz1AAkEAAAAAADRecUWAAAAANQA6R8AAAAAAoACwAAAAAgAAgAAAAAAAAABAAADwP/AAAACgAAA/9MCrQABAAAAAAAAAAAAAAAAAAAAAwABAAAAAwBVAAIAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAMCQAGQAAUAAAKZAswAAACPApkCzAAAAesAMwEJAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAg//0DwP/AAEADwABAAAAAAQAAAAAAAAAAAAAAIAAAAAAAAAIAAAACgAAxAAAAAwAAAAMAAAAcAAEAAwAAABwAAwABAAAAHAAEADAAAAAIAAgAAgAAACDpy//9//8AAAAg6cv//f///+EWNwADAAEAAAAAAAAAAAAAAAAACACEAAEAAAAAAAAAAAAAAAAxAAACAAQARAKAAsAAKwBUAAABIiYnJjQ3NzY2MzIWFxYUBwcGIicmNDc3NjQnJiYjIgYHBwYUFxYUBwYGIwciJicmNDc3NjIXFhQHBwYUFxYWMzI2Nzc2NCcmNDc2MhcWFAcHBgYjARQGDAUtLXoWOR8fORYtLTgKGwoKCjgaGg0gEhIgDXoaGgkJBQwHdR85Fi0tOAobCgoKOBoaDSASEiANehoaCQkKGwotLXoWOR8BMwUFLYEuehYXFxYugC44CQkKGwo4GkoaDQ0NDXoaShoKGwoFBe8XFi6ALjgJCQobCjgaShoNDQ0NehpKGgobCgoKLYEuehYXAAAADACWAAEAAAAAAAEACAAAAAEAAAAAAAIAAwAIAAEAAAAAAAMACAAAAAEAAAAAAAQACAAAAAEAAAAAAAUAAQALAAEAAAAAAAYACAAAAAMAAQQJAAEAEAAMAAMAAQQJAAIABgAcAAMAAQQJAAMAEAAMAAMAAQQJAAQAEAAMAAMAAQQJAAUAAgAiAAMAAQQJAAYAEAAMYW5jaG9yanM0MDBAAGEAbgBjAGgAbwByAGoAcwA0ADAAMABAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAH//wAP) format("truetype"); }',e.sheet.cssRules.length)}}(),t=document.querySelectorAll("[id]"),i=[].map.call(t,function(A){return A.id}),o=0;o\]\.\/\(\)\*\\\n\t\b\v]/g,"-").replace(/-{2,}/g,"-").substring(0,this.options.truncate).replace(/^-+|-+$/gm,"").toLowerCase()},this.hasAnchorJSLink=function(A){var e=A.firstChild&&-1<(" "+A.firstChild.className+" ").indexOf(" anchorjs-link "),t=A.lastChild&&-1<(" "+A.lastChild.className+" ").indexOf(" anchorjs-link ");return e||t||!1}}});
+// @license-end
\ No newline at end of file
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/js/ga.js b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/js/ga.js
new file mode 100644
index 0000000000..0dae3ea913
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/js/ga.js
@@ -0,0 +1,58 @@
+(function(){var $c=function(a){this.w=a||[]};$c.prototype.set=function(a){this.w[a]=!0};$c.prototype.encode=function(){for(var a=[],b=0;b\x3c/script>')):(c=M.createElement("script"),
+c.type="text/javascript",c.async=!0,c.src=a,b&&(c.id=b),a=M.getElementsByTagName("script")[0],a.parentNode.insertBefore(c,a)))},be=function(a,b){return E(M.location[b?"href":"search"],a)},E=function(a,b){return(a=a.match("(?:&|#|\\?)"+K(b).replace(/([.*+?^=!:${}()|\[\]\/\\])/g,"\\$1")+"=([^]*)"))&&2==a.length?a[1]:""},xa=function(){var a=""+M.location.hostname;return 0==a.indexOf("www.")?a.substring(4):a},de=function(a,b){var c=a.indexOf(b);if(5==c||6==c)if(a=a.charAt(c+b.length),"/"==a||"?"==a||
+""==a||":"==a)return!0;return!1},ya=function(a,b){var c=M.referrer;if(/^(https?|android-app):\/\//i.test(c)){if(a)return c;a="//"+M.location.hostname;if(!de(c,a))return b&&(b=a.replace(/\./g,"-")+".cdn.ampproject.org",de(c,b))?void 0:c}},za=function(a,b){if(1==b.length&&null!=b[0]&&"object"===typeof b[0])return b[0];for(var c={},d=Math.min(a.length+1,b.length),e=0;e=b.length)wc(a,b,c);else if(8192>=b.length)x(a,b,c)||wd(a,b,c)||wc(a,b,c);else throw ge("len",b.length),new Da(b.length);},pe=function(a,b,c,d){d=d||ua;wd(a+"?"+b,"",d,c)},wc=function(a,b,c){var d=ta(a+"?"+b);d.onload=d.onerror=function(){d.onload=null;d.onerror=null;c()}},wd=function(a,b,c,d){var e=O.XMLHttpRequest;
+if(!e)return!1;var g=new e;if(!("withCredentials"in g))return!1;a=a.replace(/^http:/,"https:");g.open("POST",a,!0);g.withCredentials=!0;g.setRequestHeader("Content-Type","text/plain");g.onreadystatechange=function(){if(4==g.readyState){if(d)try{var a=g.responseText;if(1>a.length)ge("xhr","ver","0"),c();else if("1"!=a.charAt(0))ge("xhr","ver",String(a.length)),c();else if(3=100*R(a,Ka))throw"abort";}function Ma(a){if(G(P(a,Na)))throw"abort";}function Oa(){var a=M.location.protocol;if("http:"!=a&&"https:"!=a)throw"abort";}
+function Pa(a){try{O.navigator.sendBeacon?J(42):O.XMLHttpRequest&&"withCredentials"in new O.XMLHttpRequest&&J(40)}catch(c){}a.set(ld,Td(a),!0);a.set(Ac,R(a,Ac)+1);var b=[];Qa.map(function(c,d){d.F&&(c=a.get(c),void 0!=c&&c!=d.defaultValue&&("boolean"==typeof c&&(c*=1),b.push(d.F+"="+K(""+c))))});b.push("z="+Bd());a.set(Ra,b.join("&"),!0)}
+function Sa(a){var b=P(a,gd)||oe()+"/collect",c=a.get(qe),d=P(a,fa);!d&&a.get(Vd)&&(d="beacon");if(c)pe(b,P(a,Ra),c,a.get(Ia));else if(d){c=d;d=P(a,Ra);var e=a.get(Ia);e=e||ua;"image"==c?wc(b,d,e):"xhr"==c&&wd(b,d,e)||"beacon"==c&&x(b,d,e)||ba(b,d,e)}else ba(b,P(a,Ra),a.get(Ia));b=a.get(Na);b=h(b);c=b.hitcount;b.hitcount=c?c+1:1;b=a.get(Na);delete h(b).pending_experiments;a.set(Ia,ua,!0)}
+function Hc(a){(O.gaData=O.gaData||{}).expId&&a.set(Nc,(O.gaData=O.gaData||{}).expId);(O.gaData=O.gaData||{}).expVar&&a.set(Oc,(O.gaData=O.gaData||{}).expVar);var b=a.get(Na);if(b=h(b).pending_experiments){var c=[];for(d in b)b.hasOwnProperty(d)&&b[d]&&c.push(encodeURIComponent(d)+"."+encodeURIComponent(b[d]));var d=c.join("!")}else d=void 0;d&&a.set(m,d,!0)}function cd(){if(O.navigator&&"preview"==O.navigator.loadPurpose)throw"abort";}
+function yd(a){var b=O.gaDevIds;ka(b)&&0!=b.length&&a.set("&did",b.join(","),!0)}function vb(a){if(!a.get(Na))throw"abort";};var hd=function(){return Math.round(2147483647*Math.random())},Bd=function(){try{var a=new Uint32Array(1);O.crypto.getRandomValues(a);return a[0]&2147483647}catch(b){return hd()}};function Ta(a){var b=R(a,Ua);500<=b&&J(15);var c=P(a,Va);if("transaction"!=c&&"item"!=c){c=R(a,Wa);var d=(new Date).getTime(),e=R(a,Xa);0==e&&a.set(Xa,d);e=Math.round(2*(d-e)/1E3);0=c)throw"abort";a.set(Wa,--c)}a.set(Ua,++b)};var Ya=function(){this.data=new ee},Qa=new ee,Za=[];Ya.prototype.get=function(a){var b=$a(a),c=this.data.get(a);b&&void 0==c&&(c=ea(b.defaultValue)?b.defaultValue():b.defaultValue);return b&&b.Z?b.Z(this,a,c):c};var P=function(a,b){a=a.get(b);return void 0==a?"":""+a},R=function(a,b){a=a.get(b);return void 0==a||""===a?0:1*a};Ya.prototype.set=function(a,b,c){if(a)if("object"==typeof a)for(var d in a)a.hasOwnProperty(d)&&ab(this,d,a[d],c);else ab(this,a,b,c)};
+var ab=function(a,b,c,d){if(void 0!=c)switch(b){case Na:wb.test(c)}var e=$a(b);e&&e.o?e.o(a,b,c,d):a.data.set(b,c,d)},bb=function(a,b,c,d,e){this.name=a;this.F=b;this.Z=d;this.o=e;this.defaultValue=c},$a=function(a){var b=Qa.get(a);if(!b)for(var c=0;c=b?!1:!0},gc=function(a){var b={};if(Ec(b)||Fc(b)){var c=b[Eb];void 0==c||Infinity==c||isNaN(c)||(0c)a[b]=void 0},Fd=function(a){return function(b){if("pageview"==b.get(Va)&&
+!a.I){a.I=!0;var c=aa(b),d=0a.length)J(12);else{for(var d=[],e=0;e=a&&d.push({hash:ca[0],R:e[g],O:ca})}if(0!=d.length)return 1==d.length?d[0]:Zc(b,d)||Zc(c,d)||Zc(null,d)||d[0]}function Zc(a,b){if(null==a)var c=a=1;else c=La(a),a=La(D(a,".")?a.substring(1):"."+a);for(var d=0;d=ca[0]||0>=ca[1]?"":ca.join("x");a.set(rb,c);a.set(tb,fc());a.set(ob,M.characterSet||M.charset);a.set(sb,b&&"function"===typeof b.javaEnabled&&b.javaEnabled()||
+!1);a.set(nb,(b&&(b.language||b.browserLanguage)||"").toLowerCase());a.data.set(ce,be("gclid",!0));a.data.set(ie,be("gclsrc",!0));a.data.set(fe,Math.round((new Date).getTime()/1E3));if(d&&a.get(cc)&&(b=M.location.hash)){b=b.split(/[?]+/);d=[];for(c=0;carguments.length)){if("string"===typeof arguments[0]){var b=arguments[0];var c=[].slice.call(arguments,1)}else b=arguments[0]&&arguments[0][Va],c=arguments;b&&(c=za(qc[b]||[],c),c[Va]=b,this.b.set(c,void 0,!0),this.filters.D(this.b),this.b.data.m={})}};
+pc.prototype.ma=function(a,b){var c=this;u(a,c,b)||(v(a,function(){u(a,c,b)}),y(String(c.get(V)),a,void 0,b,!0))};var rc=function(a){if("prerender"==M.visibilityState)return!1;a();return!0},z=function(a){if(!rc(a)){J(16);var b=!1,c=function(){if(!b&&rc(a)){b=!0;var d=c,e=M;e.removeEventListener?e.removeEventListener("visibilitychange",d,!1):e.detachEvent&&e.detachEvent("onvisibilitychange",d)}};L(M,"visibilitychange",c)}};var td=/^(?:(\w+)\.)?(?:(\w+):)?(\w+)$/,sc=function(a){if(ea(a[0]))this.u=a[0];else{var b=td.exec(a[0]);null!=b&&4==b.length&&(this.c=b[1]||"t0",this.K=b[2]||"",this.C=b[3],this.a=[].slice.call(a,1),this.K||(this.A="create"==this.C,this.i="require"==this.C,this.g="provide"==this.C,this.ba="remove"==this.C),this.i&&(3<=this.a.length?(this.X=this.a[1],this.W=this.a[2]):this.a[1]&&(qa(this.a[1])?this.X=this.a[1]:this.W=this.a[1])));b=a[1];a=a[2];if(!this.C)throw"abort";if(this.i&&(!qa(b)||""==b))throw"abort";
+if(this.g&&(!qa(b)||""==b||!ea(a)))throw"abort";if(ud(this.c)||ud(this.K))throw"abort";if(this.g&&"t0"!=this.c)throw"abort";}};function ud(a){return 0<=a.indexOf(".")||0<=a.indexOf(":")};var Yd,Zd,$d,A;Yd=new ee;$d=new ee;A=new ee;Zd={ec:45,ecommerce:46,linkid:47};
+var u=function(a,b,c){b==N||b.get(V);var d=Yd.get(a);if(!ea(d))return!1;b.plugins_=b.plugins_||new ee;if(b.plugins_.get(a))return!0;b.plugins_.set(a,new d(b,c||{}));return!0},y=function(a,b,c,d,e){if(!ea(Yd.get(b))&&!$d.get(b)){Zd.hasOwnProperty(b)&&J(Zd[b]);if(p.test(b)){J(52);a=N.j(a);if(!a)return!0;c=d||{};d={id:b,B:c.dataLayer||"dataLayer",ia:!!a.get("anonymizeIp"),sync:e,G:!1};a.get(">m")==b&&(d.G=!0);var g=String(a.get("name"));"t0"!=g&&(d.target=g);G(String(a.get("trackingId")))||(d.clientId=
+String(a.get(Q)),d.ka=Number(a.get(n)),c=c.palindrome?r:q,c=(c=M.cookie.replace(/^|(; +)/g,";").match(c))?c.sort().join("").substring(1):void 0,d.la=c,d.qa=E(a.b.get(kb)||"","gclid"));a=d.B;c=(new Date).getTime();O[a]=O[a]||[];c={"gtm.start":c};e||(c.event="gtm.js");O[a].push(c);c=t(d)}!c&&Zd.hasOwnProperty(b)?(J(39),c=b+".js"):J(43);c&&(c&&0<=c.indexOf("/")||(c=(Ba||"https:"==M.location.protocol?"https:":"http:")+"//www.google-analytics.com/plugins/ua/"+c),d=ae(c),a=d.protocol,c=M.location.protocol,
+("https:"==a||a==c||("http:"!=a?0:"http:"==c))&&B(d)&&(wa(d.url,void 0,e),$d.set(b,!0)))}},v=function(a,b){var c=A.get(a)||[];c.push(b);A.set(a,c)},C=function(a,b){Yd.set(a,b);b=A.get(a)||[];for(var c=0;ca.split("/")[0].indexOf(":")&&
+(a=ca+e[2].substring(0,e[2].lastIndexOf("/"))+"/"+a);c.href=a;d=b(c);return{protocol:(c.protocol||"").toLowerCase(),host:d[0],port:d[1],path:d[2],query:c.search||"",url:a||""}};var Z={ga:function(){Z.f=[]}};Z.ga();Z.D=function(a){var b=Z.J.apply(Z,arguments);b=Z.f.concat(b);for(Z.f=[];0c;c++){var d=b[c].src;if(d&&0==d.indexOf("https://www.google-analytics.com/analytics")){b=
+!0;break a}}b=!1}b&&(Ba=!0)}(O.gaplugins=O.gaplugins||{}).Linker=Dc;b=Dc.prototype;C("linker",Dc);X("decorate",b,b.ca,20);X("autoLink",b,b.S,25);C("displayfeatures",fd);C("adfeatures",fd);a=a&&a.q;ka(a)?Z.D.apply(N,a):J(50)}};N.da=function(){for(var a=N.getAll(),b=0;b>21:b}return b};})(window);
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/js/lunr/lunr.min.js b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/js/lunr/lunr.min.js
new file mode 100644
index 0000000000..f45a81eb80
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/js/lunr/lunr.min.js
@@ -0,0 +1 @@
+!function(){var t,l,c,e,r,h,d,f,p,y,m,g,x,v,w,Q,k,S,E,L,b,P,T,O,I,i,n,s,z=function(e){var t=new z.Builder;return t.pipeline.add(z.trimmer,z.stopWordFilter,z.stemmer),t.searchPipeline.add(z.stemmer),e.call(t,t),t.build()};z.version="2.3.5",z.utils={},z.utils.warn=(t=this,function(e){t.console&&console.warn&&console.warn(e)}),z.utils.asString=function(e){return null==e?"":e.toString()},z.utils.clone=function(e){if(null==e)return e;for(var t=Object.create(null),r=Object.keys(e),i=0;i=this.length)return z.QueryLexer.EOS;var e=this.str.charAt(this.pos);return this.pos+=1,e},z.QueryLexer.prototype.width=function(){return this.pos-this.start},z.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},z.QueryLexer.prototype.backup=function(){this.pos-=1},z.QueryLexer.prototype.acceptDigitRun=function(){for(var e,t;47<(t=(e=this.next()).charCodeAt(0))&&t<58;);e!=z.QueryLexer.EOS&&this.backup()},z.QueryLexer.prototype.more=function(){return this.pos document.getElementById(togglerId)
+const getTextbook = () => document.getElementById(textbookId)
+
+// [1] Run MathJax when Turbolinks navigates to a page.
+// When Turbolinks caches a page, it also saves the MathJax rendering. We mark
+// each page with a CSS class after rendering to prevent double renders when
+// navigating back to a cached page.
+document.addEventListener('turbolinks:load', () => {
+ const textbook = getTextbook()
+ if (window.MathJax && !textbook.classList.contains(mathRenderedClass)) {
+ MathJax.Hub.Queue(['Typeset', MathJax.Hub])
+ textbook.classList.add(mathRenderedClass)
+ }
+})
+
+/**
+ * [2] Toggles sidebar and menu icon
+ */
+const toggleSidebar = () => {
+ const toggler = getToggler()
+ const textbook = getTextbook()
+
+ if (textbook.classList.contains(textbookActiveClass)) {
+ textbook.classList.remove(textbookActiveClass)
+ toggler.classList.remove(togglerActiveClass)
+ } else {
+ textbook.classList.add(textbookActiveClass)
+ toggler.classList.add(togglerActiveClass)
+ }
+}
+
+/**
+ * Keep the variable below in sync with the tablet breakpoint value in
+ * _sass/inuitcss/tools/_tools.mq.scss
+ *
+ */
+const autoCloseSidebarBreakpoint = 740
+
+// Set up event listener for sidebar toggle button
+const sidebarButtonHandler = () => {
+ getToggler().addEventListener('click', toggleSidebar)
+
+ /**
+ * Auto-close sidebar on smaller screens after page load.
+ *
+ * Having the sidebar be open by default then closing it on page load for
+ * small screens gives the illusion that the sidebar closes in response
+ * to selecting a page in the sidebar. However, it does cause a bit of jank
+ * on the first page load.
+ *
+ * Since we don't want to persist state in between page navigation, this is
+ * the best we can do while optimizing for larger screens where most
+ * viewers will read the textbook.
+ *
+ * The code below assumes that the sidebar is open by default.
+ */
+ if (window.innerWidth < autoCloseSidebarBreakpoint) toggleSidebar()
+}
+
+initFunction(sidebarButtonHandler);
+
+/**
+ * [3] Preserve sidebar scroll when navigating between pages
+ */
+let sidebarScrollTop = 0
+const getSidebar = () => document.getElementById('js-sidebar')
+
+document.addEventListener('turbolinks:before-visit', () => {
+ sidebarScrollTop = getSidebar().scrollTop
+})
+
+document.addEventListener('turbolinks:load', () => {
+ getSidebar().scrollTop = sidebarScrollTop
+})
+
+/**
+ * Focus textbook page by default so that user can scroll with spacebar
+ */
+const focusPage = () => {
+ document.querySelector('.c-textbook__page').focus()
+}
+
+initFunction(focusPage);
+
+/**
+ * [4] Use left and right arrow keys to navigate forward and backwards.
+ */
+const LEFT_ARROW_KEYCODE = 37
+const RIGHT_ARROW_KEYCODE = 39
+
+const getPrevUrl = () => document.getElementById('js-page__nav__prev').href
+const getNextUrl = () => document.getElementById('js-page__nav__next').href
+const initPageNav = (event) => {
+ const keycode = event.which
+
+ if (keycode === LEFT_ARROW_KEYCODE) {
+ Turbolinks.visit(getPrevUrl())
+ } else if (keycode === RIGHT_ARROW_KEYCODE) {
+ Turbolinks.visit(getNextUrl())
+ }
+};
+
+var keyboardListener = false;
+const initListener = () => {
+ if (keyboardListener === false) {
+ document.addEventListener('keydown', initPageNav)
+ keyboardListener = true;
+ }
+}
+initFunction(initListener);
+
+/**
+ * [5] Right sidebar scroll highlighting
+ */
+
+highlightRightSidebar = function() {
+ var position = document.querySelector('.c-textbook__page').scrollTop;
+ position = position + (window.innerHeight / 4); // + Manual offset
+
+ // Highlight the "active" menu item
+ document.querySelectorAll('.c-textbook__content h2, .c-textbook__content h3').forEach((header, index) => {
+ var target = header.offsetTop;
+ var id = header.id;
+ if (position >= target) {
+ var query = 'ul.toc__menu a[href="#' + id + '"]';
+ document.querySelectorAll('ul.toc__menu li').forEach((item) => {item.classList.remove('active')});
+ document.querySelectorAll(query).forEach((item) => {item.parentElement.classList.add('active')});
+ }
+ });
+ document.querySelector('.c-textbook__page').addEventListener('scroll', highlightRightSidebar);
+};
+
+initFunction(highlightRightSidebar);
diff --git a/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/js/tocbot.min.js b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/js/tocbot.min.js
new file mode 100644
index 0000000000..943d8fdb77
--- /dev/null
+++ b/extensions/azurehybridtoolkit/notebooks/hybridbook/assets/js/tocbot.min.js
@@ -0,0 +1 @@
+!function(e){function t(o){if(n[o])return n[o].exports;var l=n[o]={i:o,l:!1,exports:{}};return e[o].call(l.exports,l,l.exports,t),l.l=!0,l.exports}var n={};t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:o})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=0)}([function(e,t,n){(function(o){var l,i,s;!function(n,o){i=[],l=o(n),void 0!==(s="function"==typeof l?l.apply(t,i):l)&&(e.exports=s)}(void 0!==o?o:this.window||this.global,function(e){"use strict";function t(){for(var e={},t=0;te.fixedSidebarOffset?-1===n.className.indexOf(e.positionFixedClass)&&(n.className+=h+e.positionFixedClass):n.className=n.className.split(h+e.positionFixedClass).join("")}function s(t){var n=document.documentElement.scrollTop||f.scrollTop;e.positionFixedSelector&&i();var o,l=t;if(m&&null!==document.querySelector(e.tocSelector)&&l.length>0){d.call(l,function(t,i){if(t.offsetTop>n+e.headingsOffset+10){return o=l[0===i?i:i-1],!0}if(i===l.length-1)return o=l[l.length-1],!0});var s=document.querySelector(e.tocSelector).querySelectorAll("."+e.linkClass);u.call(s,function(t){t.className=t.className.split(h+e.activeLinkClass).join("")});var c=document.querySelector(e.tocSelector).querySelectorAll("."+e.listItemClass);u.call(c,function(t){t.className=t.className.split(h+e.activeListItemClass).join("")});var a=document.querySelector(e.tocSelector).querySelector("."+e.linkClass+".node-name--"+o.nodeName+'[href="#'+o.id+'"]');-1===a.className.indexOf(e.activeLinkClass)&&(a.className+=h+e.activeLinkClass);var p=a.parentNode;p&&-1===p.className.indexOf(e.activeListItemClass)&&(p.className+=h+e.activeListItemClass);var C=document.querySelector(e.tocSelector).querySelectorAll("."+e.listClass+"."+e.collapsibleClass);u.call(C,function(t){-1===t.className.indexOf(e.isCollapsedClass)&&(t.className+=h+e.isCollapsedClass)}),a.nextSibling&&-1!==a.nextSibling.className.indexOf(e.isCollapsedClass)&&(a.nextSibling.className=a.nextSibling.className.split(h+e.isCollapsedClass).join("")),r(a.parentNode.parentNode)}}function r(t){return-1!==t.className.indexOf(e.collapsibleClass)&&-1!==t.className.indexOf(e.isCollapsedClass)?(t.className=t.className.split(h+e.isCollapsedClass).join(""),r(t.parentNode.parentNode)):t}function c(t){var n=t.target||t.srcElement;"string"==typeof n.className&&-1!==n.className.indexOf(e.linkClass)&&(m=!1)}function a(){m=!0}var u=[].forEach,d=[].some,f=document.body,m=!0,h=" ";return{enableTocAnimation:a,disableTocAnimation:c,render:n,updateToc:s}}},function(e,t){e.exports=function(e){function t(e){return e[e.length-1]}function n(e){return+e.nodeName.split("H").join("")}function o(t){var o={id:t.id,children:[],nodeName:t.nodeName,headingLevel:n(t),textContent:t.textContent.trim()};return e.includeHtml&&(o.childNodes=t.childNodes),o}function l(l,i){for(var s=o(l),r=n(l),c=i,a=t(c),u=a?a.headingLevel:0,d=r-u;d>0;)a=t(c),a&&void 0!==a.children&&(c=a.children),d--;return r>=e.collapseDepth&&(s.isCollapsed=!0),c.push(s),c}function i(t,n){var o=n;e.ignoreSelector&&(o=n.split(",").map(function(t){return t.trim()+":not("+e.ignoreSelector+")"}));try{return document.querySelector(t).querySelectorAll(o)}catch(e){return console.warn("Element not found: "+t),null}}function s(e){return r.call(e,function(e,t){return l(o(t),e.nest),e},{nest:[]})}var r=[].reduce;return{nestHeadingsArray:s,selectHeadings:i}}},function(e,t){function n(e){function t(e){return"a"===e.tagName.toLowerCase()&&(e.hash.length>0||"#"===e.href.charAt(e.href.length-1))&&(n(e.href)===s||n(e.href)+"#"===s)}function n(e){return e.slice(0,e.lastIndexOf("#"))}function l(e){var t=document.getElementById(e.substring(1));t&&(/^(?:a|select|input|button|textarea)$/i.test(t.tagName)||(t.tabIndex=-1),t.focus())}!function(){document.documentElement.style}();var i=e.duration,s=location.hash?n(location.href):location.href;!function(){function n(n){!t(n.target)||n.target.className.indexOf("no-smooth-scroll")>-1||"#"===n.target.href.charAt(n.target.href.length-2)&&"!"===n.target.href.charAt(n.target.href.length-1)||-1===n.target.className.indexOf(e.linkClass)||o(n.target.hash,{duration:i,callback:function(){l(n.target.hash)}})}document.body.addEventListener("click",n,!1)}()}function o(e,t){function n(e){s=e-i,window.scrollTo(0,c.easing(s,r,u,d)),s=e;t=++e)r+=9===t||14===t||19===t||24===t?"-":15===t?"4":20===t?(Math.floor(4*Math.random())+8).toString(16):Math.floor(15*Math.random()).toString(16);return r}}).call(this),function(){e.Location=function(){function t(t){var e,r;null==t&&(t=""),r=document.createElement("a"),r.href=t.toString(),this.absoluteURL=r.href,e=r.hash.length,2>e?this.requestURL=this.absoluteURL:(this.requestURL=this.absoluteURL.slice(0,-e),this.anchor=r.hash.slice(1))}var e,r,n,o;return t.wrap=function(t){return t instanceof this?t:new this(t)},t.prototype.getOrigin=function(){return this.absoluteURL.split("/",3).join("/")},t.prototype.getPath=function(){var t,e;return null!=(t=null!=(e=this.requestURL.match(/\/\/[^\/]*(\/[^?;]*)/))?e[1]:void 0)?t:"/"},t.prototype.getPathComponents=function(){return this.getPath().split("/").slice(1)},t.prototype.getLastPathComponent=function(){return this.getPathComponents().slice(-1)[0]},t.prototype.getExtension=function(){var t,e;return null!=(t=null!=(e=this.getLastPathComponent().match(/\.[^.]*$/))?e[0]:void 0)?t:""},t.prototype.isHTML=function(){return this.getExtension().match(/^(?:|\.(?:htm|html|xhtml))$/)},t.prototype.isPrefixedBy=function(t){var e;return e=r(t),this.isEqualTo(t)||o(this.absoluteURL,e)},t.prototype.isEqualTo=function(t){return this.absoluteURL===(null!=t?t.absoluteURL:void 0)},t.prototype.toCacheKey=function(){return this.requestURL},t.prototype.toJSON=function(){return this.absoluteURL},t.prototype.toString=function(){return this.absoluteURL},t.prototype.valueOf=function(){return this.absoluteURL},r=function(t){return e(t.getOrigin()+t.getPath())},e=function(t){return n(t,"/")?t:t+"/"},o=function(t,e){return t.slice(0,e.length)===e},n=function(t,e){return t.slice(-e.length)===e},t}()}.call(this),function(){var t=function(t,e){return function(){return t.apply(e,arguments)}};e.HttpRequest=function(){function r(r,n,o){this.delegate=r,this.requestCanceled=t(this.requestCanceled,this),this.requestTimedOut=t(this.requestTimedOut,this),this.requestFailed=t(this.requestFailed,this),this.requestLoaded=t(this.requestLoaded,this),this.requestProgressed=t(this.requestProgressed,this),this.url=e.Location.wrap(n).requestURL,this.referrer=e.Location.wrap(o).absoluteURL,this.createXHR()}return r.NETWORK_FAILURE=0,r.TIMEOUT_FAILURE=-1,r.timeout=60,r.prototype.send=function(){var t;return this.xhr&&!this.sent?(this.notifyApplicationBeforeRequestStart(),this.setProgress(0),this.xhr.send(),this.sent=!0,"function"==typeof(t=this.delegate).requestStarted?t.requestStarted():void 0):void 0},r.prototype.cancel=function(){return this.xhr&&this.sent?this.xhr.abort():void 0},r.prototype.requestProgressed=function(t){return t.lengthComputable?this.setProgress(t.loaded/t.total):void 0},r.prototype.requestLoaded=function(){return this.endRequest(function(t){return function(){var e;return 200<=(e=t.xhr.status)&&300>e?t.delegate.requestCompletedWithResponse(t.xhr.responseText,t.xhr.getResponseHeader("Turbolinks-Location")):(t.failed=!0,t.delegate.requestFailedWithStatusCode(t.xhr.status,t.xhr.responseText))}}(this))},r.prototype.requestFailed=function(){return this.endRequest(function(t){return function(){return t.failed=!0,t.delegate.requestFailedWithStatusCode(t.constructor.NETWORK_FAILURE)}}(this))},r.prototype.requestTimedOut=function(){return this.endRequest(function(t){return function(){return t.failed=!0,t.delegate.requestFailedWithStatusCode(t.constructor.TIMEOUT_FAILURE)}}(this))},r.prototype.requestCanceled=function(){return this.endRequest()},r.prototype.notifyApplicationBeforeRequestStart=function(){return e.dispatch("turbolinks:request-start",{data:{url:this.url,xhr:this.xhr}})},r.prototype.notifyApplicationAfterRequestEnd=function(){return e.dispatch("turbolinks:request-end",{data:{url:this.url,xhr:this.xhr}})},r.prototype.createXHR=function(){return this.xhr=new XMLHttpRequest,this.xhr.open("GET",this.url,!0),this.xhr.timeout=1e3*this.constructor.timeout,this.xhr.setRequestHeader("Accept","text/html, application/xhtml+xml"),this.xhr.setRequestHeader("Turbolinks-Referrer",this.referrer),this.xhr.onprogress=this.requestProgressed,this.xhr.onload=this.requestLoaded,this.xhr.onerror=this.requestFailed,this.xhr.ontimeout=this.requestTimedOut,this.xhr.onabort=this.requestCanceled},r.prototype.endRequest=function(t){return this.xhr?(this.notifyApplicationAfterRequestEnd(),null!=t&&t.call(this),this.destroy()):void 0},r.prototype.setProgress=function(t){var e;return this.progress=t,"function"==typeof(e=this.delegate).requestProgressed?e.requestProgressed(this.progress):void 0},r.prototype.destroy=function(){var t;return this.setProgress(1),"function"==typeof(t=this.delegate).requestFinished&&t.requestFinished(),this.delegate=null,this.xhr=null},r}()}.call(this),function(){var t=function(t,e){return function(){return t.apply(e,arguments)}};e.ProgressBar=function(){function e(){this.trickle=t(this.trickle,this),this.stylesheetElement=this.createStylesheetElement(),this.progressElement=this.createProgressElement()}var r;return r=300,e.defaultCSS=".turbolinks-progress-bar {\n position: fixed;\n display: block;\n top: 0;\n left: 0;\n height: 3px;\n background: #0076ff;\n z-index: 9999;\n transition: width "+r+"ms ease-out, opacity "+r/2+"ms "+r/2+"ms ease-in;\n transform: translate3d(0, 0, 0);\n}",e.prototype.show=function(){return this.visible?void 0:(this.visible=!0,this.installStylesheetElement(),this.installProgressElement(),this.startTrickling())},e.prototype.hide=function(){return this.visible&&!this.hiding?(this.hiding=!0,this.fadeProgressElement(function(t){return function(){return t.uninstallProgressElement(),t.stopTrickling(),t.visible=!1,t.hiding=!1}}(this))):void 0},e.prototype.setValue=function(t){return this.value=t,this.refresh()},e.prototype.installStylesheetElement=function(){return document.head.insertBefore(this.stylesheetElement,document.head.firstChild)},e.prototype.installProgressElement=function(){return this.progressElement.style.width=0,this.progressElement.style.opacity=1,document.documentElement.insertBefore(this.progressElement,document.body),this.refresh()},e.prototype.fadeProgressElement=function(t){return this.progressElement.style.opacity=0,setTimeout(t,1.5*r)},e.prototype.uninstallProgressElement=function(){return this.progressElement.parentNode?document.documentElement.removeChild(this.progressElement):void 0},e.prototype.startTrickling=function(){return null!=this.trickleInterval?this.trickleInterval:this.trickleInterval=setInterval(this.trickle,r)},e.prototype.stopTrickling=function(){return clearInterval(this.trickleInterval),this.trickleInterval=null},e.prototype.trickle=function(){return this.setValue(this.value+Math.random()/100)},e.prototype.refresh=function(){return requestAnimationFrame(function(t){return function(){return t.progressElement.style.width=10+90*t.value+"%"}}(this))},e.prototype.createStylesheetElement=function(){var t;return t=document.createElement("style"),t.type="text/css",t.textContent=this.constructor.defaultCSS,t},e.prototype.createProgressElement=function(){var t;return t=document.createElement("div"),t.className="turbolinks-progress-bar",t},e}()}.call(this),function(){var t=function(t,e){return function(){return t.apply(e,arguments)}};e.BrowserAdapter=function(){function r(r){this.controller=r,this.showProgressBar=t(this.showProgressBar,this),this.progressBar=new e.ProgressBar}var n,o,i;return i=e.HttpRequest,n=i.NETWORK_FAILURE,o=i.TIMEOUT_FAILURE,r.prototype.visitProposedToLocationWithAction=function(t,e){return this.controller.startVisitToLocationWithAction(t,e)},r.prototype.visitStarted=function(t){return t.issueRequest(),t.changeHistory(),t.loadCachedSnapshot()},r.prototype.visitRequestStarted=function(t){return this.progressBar.setValue(0),t.hasCachedSnapshot()||"restore"!==t.action?this.showProgressBarAfterDelay():this.showProgressBar()},r.prototype.visitRequestProgressed=function(t){return this.progressBar.setValue(t.progress)},r.prototype.visitRequestCompleted=function(t){return t.loadResponse()},r.prototype.visitRequestFailedWithStatusCode=function(t,e){switch(e){case n:case o:return this.reload();default:return t.loadResponse()}},r.prototype.visitRequestFinished=function(t){return this.hideProgressBar()},r.prototype.visitCompleted=function(t){return t.followRedirect()},r.prototype.pageInvalidated=function(){return this.reload()},r.prototype.showProgressBarAfterDelay=function(){return this.progressBarTimeout=setTimeout(this.showProgressBar,this.controller.progressBarDelay)},r.prototype.showProgressBar=function(){return this.progressBar.show()},r.prototype.hideProgressBar=function(){return this.progressBar.hide(),clearTimeout(this.progressBarTimeout)},r.prototype.reload=function(){return window.location.reload()},r}()}.call(this),function(){var t=function(t,e){return function(){return t.apply(e,arguments)}};e.History=function(){function r(e){this.delegate=e,this.onPageLoad=t(this.onPageLoad,this),this.onPopState=t(this.onPopState,this)}return r.prototype.start=function(){return this.started?void 0:(addEventListener("popstate",this.onPopState,!1),addEventListener("load",this.onPageLoad,!1),this.started=!0)},r.prototype.stop=function(){return this.started?(removeEventListener("popstate",this.onPopState,!1),removeEventListener("load",this.onPageLoad,!1),this.started=!1):void 0},r.prototype.push=function(t,r){return t=e.Location.wrap(t),this.update("push",t,r)},r.prototype.replace=function(t,r){return t=e.Location.wrap(t),this.update("replace",t,r)},r.prototype.onPopState=function(t){var r,n,o,i;return this.shouldHandlePopState()&&(i=null!=(n=t.state)?n.turbolinks:void 0)?(r=e.Location.wrap(window.location),o=i.restorationIdentifier,this.delegate.historyPoppedToLocationWithRestorationIdentifier(r,o)):void 0},r.prototype.onPageLoad=function(t){return e.defer(function(t){return function(){return t.pageLoaded=!0}}(this))},r.prototype.shouldHandlePopState=function(){return this.pageIsLoaded()},r.prototype.pageIsLoaded=function(){return this.pageLoaded||"complete"===document.readyState},r.prototype.update=function(t,e,r){var n;return n={turbolinks:{restorationIdentifier:r}},history[t+"State"](n,null,e)},r}()}.call(this),function(){e.HeadDetails=function(){function t(t){var e,r,n,s,a,u;for(this.elements={},n=0,a=t.length;a>n;n++)u=t[n],u.nodeType===Node.ELEMENT_NODE&&(s=u.outerHTML,r=null!=(e=this.elements)[s]?e[s]:e[s]={type:i(u),tracked:o(u),elements:[]},r.elements.push(u))}var e,r,n,o,i;return t.fromHeadElement=function(t){var e;return new this(null!=(e=null!=t?t.childNodes:void 0)?e:[])},t.prototype.hasElementWithKey=function(t){return t in this.elements},t.prototype.getTrackedElementSignature=function(){var t,e;return function(){var r,n;r=this.elements,n=[];for(t in r)e=r[t].tracked,e&&n.push(t);return n}.call(this).join("")},t.prototype.getScriptElementsNotInDetails=function(t){return this.getElementsMatchingTypeNotInDetails("script",t)},t.prototype.getStylesheetElementsNotInDetails=function(t){return this.getElementsMatchingTypeNotInDetails("stylesheet",t)},t.prototype.getElementsMatchingTypeNotInDetails=function(t,e){var r,n,o,i,s,a;o=this.elements,s=[];for(n in o)i=o[n],a=i.type,r=i.elements,a!==t||e.hasElementWithKey(n)||s.push(r[0]);return s},t.prototype.getProvisionalElements=function(){var t,e,r,n,o,i,s;r=[],n=this.elements;for(e in n)o=n[e],s=o.type,i=o.tracked,t=o.elements,null!=s||i?t.length>1&&r.push.apply(r,t.slice(1)):r.push.apply(r,t);return r},t.prototype.getMetaValue=function(t){var e;return null!=(e=this.findMetaElementByName(t))?e.getAttribute("content"):void 0},t.prototype.findMetaElementByName=function(t){var r,n,o,i;r=void 0,i=this.elements;for(o in i)n=i[o].elements,e(n[0],t)&&(r=n[0]);return r},i=function(t){return r(t)?"script":n(t)?"stylesheet":void 0},o=function(t){return"reload"===t.getAttribute("data-turbolinks-track")},r=function(t){var e;return e=t.tagName.toLowerCase(),"script"===e},n=function(t){var e;return e=t.tagName.toLowerCase(),"style"===e||"link"===e&&"stylesheet"===t.getAttribute("rel")},e=function(t,e){var r;return r=t.tagName.toLowerCase(),"meta"===r&&t.getAttribute("name")===e},t}()}.call(this),function(){e.Snapshot=function(){function t(t,e){this.headDetails=t,this.bodyElement=e}return t.wrap=function(t){return t instanceof this?t:"string"==typeof t?this.fromHTMLString(t):this.fromHTMLElement(t)},t.fromHTMLString=function(t){var e;return e=document.createElement("html"),e.innerHTML=t,this.fromHTMLElement(e)},t.fromHTMLElement=function(t){var r,n,o,i;return o=t.querySelector("head"),r=null!=(i=t.querySelector("body"))?i:document.createElement("body"),n=e.HeadDetails.fromHeadElement(o),new this(n,r)},t.prototype.clone=function(){return new this.constructor(this.headDetails,this.bodyElement.cloneNode(!0))},t.prototype.getRootLocation=function(){var t,r;return r=null!=(t=this.getSetting("root"))?t:"/",new e.Location(r)},t.prototype.getCacheControlValue=function(){return this.getSetting("cache-control")},t.prototype.getElementForAnchor=function(t){try{return this.bodyElement.querySelector("[id='"+t+"'], a[name='"+t+"']")}catch(e){}},t.prototype.getPermanentElements=function(){return this.bodyElement.querySelectorAll("[id][data-turbolinks-permanent]")},t.prototype.getPermanentElementById=function(t){return this.bodyElement.querySelector("#"+t+"[data-turbolinks-permanent]")},t.prototype.getPermanentElementsPresentInSnapshot=function(t){var e,r,n,o,i;for(o=this.getPermanentElements(),i=[],r=0,n=o.length;n>r;r++)e=o[r],t.getPermanentElementById(e.id)&&i.push(e);return i},t.prototype.findFirstAutofocusableElement=function(){return this.bodyElement.querySelector("[autofocus]")},t.prototype.hasAnchor=function(t){return null!=this.getElementForAnchor(t)},t.prototype.isPreviewable=function(){return"no-preview"!==this.getCacheControlValue()},t.prototype.isCacheable=function(){return"no-cache"!==this.getCacheControlValue()},t.prototype.isVisitable=function(){return"reload"!==this.getSetting("visit-control")},t.prototype.getSetting=function(t){return this.headDetails.getMetaValue("turbolinks-"+t)},t}()}.call(this),function(){var t=[].slice;e.Renderer=function(){function e(){}var r;return e.render=function(){var e,r,n,o;return n=arguments[0],r=arguments[1],e=3<=arguments.length?t.call(arguments,2):[],o=function(t,e,r){r.prototype=t.prototype;var n=new r,o=t.apply(n,e);return Object(o)===o?o:n}(this,e,function(){}),o.delegate=n,o.render(r),o},e.prototype.renderView=function(t){return this.delegate.viewWillRender(this.newBody),t(),this.delegate.viewRendered(this.newBody)},e.prototype.invalidateView=function(){return this.delegate.viewInvalidated()},e.prototype.createScriptElement=function(t){var e;return"false"===t.getAttribute("data-turbolinks-eval")?t:(e=document.createElement("script"),e.textContent=t.textContent,e.async=!1,r(e,t),e)},r=function(t,e){var r,n,o,i,s,a,u;for(i=e.attributes,a=[],r=0,n=i.length;n>r;r++)s=i[r],o=s.name,u=s.value,a.push(t.setAttribute(o,u));return a},e}()}.call(this),function(){var t,r,n=function(t,e){function r(){this.constructor=t}for(var n in e)o.call(e,n)&&(t[n]=e[n]);return r.prototype=e.prototype,t.prototype=new r,t.__super__=e.prototype,t},o={}.hasOwnProperty;e.SnapshotRenderer=function(e){function o(t,e,r){this.currentSnapshot=t,this.newSnapshot=e,this.isPreview=r,this.currentHeadDetails=this.currentSnapshot.headDetails,this.newHeadDetails=this.newSnapshot.headDetails,this.currentBody=this.currentSnapshot.bodyElement,this.newBody=this.newSnapshot.bodyElement}return n(o,e),o.prototype.render=function(t){return this.shouldRender()?(this.mergeHead(),this.renderView(function(e){return function(){return e.replaceBody(),e.isPreview||e.focusFirstAutofocusableElement(),t()}}(this))):this.invalidateView()},o.prototype.mergeHead=function(){return this.copyNewHeadStylesheetElements(),this.copyNewHeadScriptElements(),this.removeCurrentHeadProvisionalElements(),this.copyNewHeadProvisionalElements()},o.prototype.replaceBody=function(){var t;return t=this.relocateCurrentBodyPermanentElements(),this.activateNewBodyScriptElements(),this.assignNewBody(),this.replacePlaceholderElementsWithClonedPermanentElements(t)},o.prototype.shouldRender=function(){return this.newSnapshot.isVisitable()&&this.trackedElementsAreIdentical()},o.prototype.trackedElementsAreIdentical=function(){return this.currentHeadDetails.getTrackedElementSignature()===this.newHeadDetails.getTrackedElementSignature()},o.prototype.copyNewHeadStylesheetElements=function(){var t,e,r,n,o;for(n=this.getNewHeadStylesheetElements(),o=[],e=0,r=n.length;r>e;e++)t=n[e],o.push(document.head.appendChild(t));return o},o.prototype.copyNewHeadScriptElements=function(){var t,e,r,n,o;for(n=this.getNewHeadScriptElements(),o=[],e=0,r=n.length;r>e;e++)t=n[e],o.push(document.head.appendChild(this.createScriptElement(t)));return o},o.prototype.removeCurrentHeadProvisionalElements=function(){var t,e,r,n,o;for(n=this.getCurrentHeadProvisionalElements(),o=[],e=0,r=n.length;r>e;e++)t=n[e],o.push(document.head.removeChild(t));return o},o.prototype.copyNewHeadProvisionalElements=function(){var t,e,r,n,o;for(n=this.getNewHeadProvisionalElements(),o=[],e=0,r=n.length;r>e;e++)t=n[e],o.push(document.head.appendChild(t));return o},o.prototype.relocateCurrentBodyPermanentElements=function(){var e,n,o,i,s,a,u;for(a=this.getCurrentBodyPermanentElements(),u=[],e=0,n=a.length;n>e;e++)i=a[e],s=t(i),o=this.newSnapshot.getPermanentElementById(i.id),r(i,s.element),r(o,i),u.push(s);return u},o.prototype.replacePlaceholderElementsWithClonedPermanentElements=function(t){var e,n,o,i,s,a,u;for(u=[],o=0,i=t.length;i>o;o++)a=t[o],n=a.element,s=a.permanentElement,e=s.cloneNode(!0),u.push(r(n,e));return u},o.prototype.activateNewBodyScriptElements=function(){var t,e,n,o,i,s;for(i=this.getNewBodyScriptElements(),s=[],e=0,o=i.length;o>e;e++)n=i[e],t=this.createScriptElement(n),s.push(r(n,t));return s},o.prototype.assignNewBody=function(){return document.body=this.newBody},o.prototype.focusFirstAutofocusableElement=function(){var t;return null!=(t=this.newSnapshot.findFirstAutofocusableElement())?t.focus():void 0},o.prototype.getNewHeadStylesheetElements=function(){return this.newHeadDetails.getStylesheetElementsNotInDetails(this.currentHeadDetails)},o.prototype.getNewHeadScriptElements=function(){return this.newHeadDetails.getScriptElementsNotInDetails(this.currentHeadDetails)},o.prototype.getCurrentHeadProvisionalElements=function(){return this.currentHeadDetails.getProvisionalElements()},o.prototype.getNewHeadProvisionalElements=function(){return this.newHeadDetails.getProvisionalElements()},o.prototype.getCurrentBodyPermanentElements=function(){return this.currentSnapshot.getPermanentElementsPresentInSnapshot(this.newSnapshot)},o.prototype.getNewBodyScriptElements=function(){return this.newBody.querySelectorAll("script")},o}(e.Renderer),t=function(t){var e;return e=document.createElement("meta"),e.setAttribute("name","turbolinks-permanent-placeholder"),e.setAttribute("content",t.id),{element:e,permanentElement:t}},r=function(t,e){var r;return(r=t.parentNode)?r.replaceChild(e,t):void 0}}.call(this),function(){var t=function(t,e){function n(){this.constructor=t}for(var o in e)r.call(e,o)&&(t[o]=e[o]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},r={}.hasOwnProperty;e.ErrorRenderer=function(e){function r(t){var e;e=document.createElement("html"),e.innerHTML=t,this.newHead=e.querySelector("head"),this.newBody=e.querySelector("body")}return t(r,e),r.prototype.render=function(t){return this.renderView(function(e){return function(){return e.replaceHeadAndBody(),e.activateBodyScriptElements(),t()}}(this))},r.prototype.replaceHeadAndBody=function(){var t,e;return e=document.head,t=document.body,e.parentNode.replaceChild(this.newHead,e),t.parentNode.replaceChild(this.newBody,t)},r.prototype.activateBodyScriptElements=function(){var t,e,r,n,o,i;for(n=this.getScriptElements(),i=[],e=0,r=n.length;r>e;e++)o=n[e],t=this.createScriptElement(o),i.push(o.parentNode.replaceChild(t,o));return i},r.prototype.getScriptElements=function(){return document.documentElement.querySelectorAll("script")},r}(e.Renderer)}.call(this),function(){e.View=function(){function t(t){this.delegate=t,this.htmlElement=document.documentElement}return t.prototype.getRootLocation=function(){return this.getSnapshot().getRootLocation()},t.prototype.getElementForAnchor=function(t){return this.getSnapshot().getElementForAnchor(t)},t.prototype.getSnapshot=function(){return e.Snapshot.fromHTMLElement(this.htmlElement)},t.prototype.render=function(t,e){var r,n,o;return o=t.snapshot,r=t.error,n=t.isPreview,this.markAsPreview(n),null!=o?this.renderSnapshot(o,n,e):this.renderError(r,e)},t.prototype.markAsPreview=function(t){return t?this.htmlElement.setAttribute("data-turbolinks-preview",""):this.htmlElement.removeAttribute("data-turbolinks-preview")},t.prototype.renderSnapshot=function(t,r,n){return e.SnapshotRenderer.render(this.delegate,n,this.getSnapshot(),e.Snapshot.wrap(t),r)},t.prototype.renderError=function(t,r){return e.ErrorRenderer.render(this.delegate,r,t)},t}()}.call(this),function(){var t=function(t,e){return function(){return t.apply(e,arguments)}};e.ScrollManager=function(){function r(r){this.delegate=r,this.onScroll=t(this.onScroll,this),this.onScroll=e.throttle(this.onScroll)}return r.prototype.start=function(){return this.started?void 0:(addEventListener("scroll",this.onScroll,!1),this.onScroll(),this.started=!0)},r.prototype.stop=function(){return this.started?(removeEventListener("scroll",this.onScroll,!1),this.started=!1):void 0},r.prototype.scrollToElement=function(t){return t.scrollIntoView()},r.prototype.scrollToPosition=function(t){var e,r;return e=t.x,r=t.y,window.scrollTo(e,r)},r.prototype.onScroll=function(t){return this.updatePosition({x:window.pageXOffset,y:window.pageYOffset})},r.prototype.updatePosition=function(t){var e;return this.position=t,null!=(e=this.delegate)?e.scrollPositionChanged(this.position):void 0},r}()}.call(this),function(){e.SnapshotCache=function(){function t(t){this.size=t,this.keys=[],this.snapshots={}}var r;return t.prototype.has=function(t){var e;return e=r(t),e in this.snapshots},t.prototype.get=function(t){var e;if(this.has(t))return e=this.read(t),this.touch(t),e},t.prototype.put=function(t,e){return this.write(t,e),this.touch(t),e},t.prototype.read=function(t){var e;return e=r(t),this.snapshots[e]},t.prototype.write=function(t,e){var n;return n=r(t),this.snapshots[n]=e},t.prototype.touch=function(t){var e,n;return n=r(t),e=this.keys.indexOf(n),e>-1&&this.keys.splice(e,1),this.keys.unshift(n),this.trim()},t.prototype.trim=function(){var t,e,r,n,o;for(n=this.keys.splice(this.size),o=[],t=0,r=n.length;r>t;t++)e=n[t],o.push(delete this.snapshots[e]);return o},r=function(t){return e.Location.wrap(t).toCacheKey()},t}()}.call(this),function(){var t=function(t,e){return function(){return t.apply(e,arguments)}};e.Visit=function(){function r(r,n,o){this.controller=r,this.action=o,this.performScroll=t(this.performScroll,this),this.identifier=e.uuid(),this.location=e.Location.wrap(n),this.adapter=this.controller.adapter,this.state="initialized",this.timingMetrics={}}var n;return r.prototype.start=function(){return"initialized"===this.state?(this.recordTimingMetric("visitStart"),this.state="started",this.adapter.visitStarted(this)):void 0},r.prototype.cancel=function(){var t;return"started"===this.state?(null!=(t=this.request)&&t.cancel(),this.cancelRender(),this.state="canceled"):void 0},r.prototype.complete=function(){var t;return"started"===this.state?(this.recordTimingMetric("visitEnd"),this.state="completed","function"==typeof(t=this.adapter).visitCompleted&&t.visitCompleted(this),this.controller.visitCompleted(this)):void 0},r.prototype.fail=function(){var t;return"started"===this.state?(this.state="failed","function"==typeof(t=this.adapter).visitFailed?t.visitFailed(this):void 0):void 0},r.prototype.changeHistory=function(){var t,e;return this.historyChanged?void 0:(t=this.location.isEqualTo(this.referrer)?"replace":this.action,e=n(t),this.controller[e](this.location,this.restorationIdentifier),this.historyChanged=!0)},r.prototype.issueRequest=function(){return this.shouldIssueRequest()&&null==this.request?(this.progress=0,this.request=new e.HttpRequest(this,this.location,this.referrer),this.request.send()):void 0},r.prototype.getCachedSnapshot=function(){var t;return!(t=this.controller.getCachedSnapshotForLocation(this.location))||null!=this.location.anchor&&!t.hasAnchor(this.location.anchor)||"restore"!==this.action&&!t.isPreviewable()?void 0:t},r.prototype.hasCachedSnapshot=function(){return null!=this.getCachedSnapshot()},r.prototype.loadCachedSnapshot=function(){var t,e;return(e=this.getCachedSnapshot())?(t=this.shouldIssueRequest(),this.render(function(){var r;return this.cacheSnapshot(),this.controller.render({snapshot:e,isPreview:t},this.performScroll),"function"==typeof(r=this.adapter).visitRendered&&r.visitRendered(this),t?void 0:this.complete()})):void 0},r.prototype.loadResponse=function(){return null!=this.response?this.render(function(){var t,e;return this.cacheSnapshot(),this.request.failed?(this.controller.render({error:this.response},this.performScroll),"function"==typeof(t=this.adapter).visitRendered&&t.visitRendered(this),this.fail()):(this.controller.render({snapshot:this.response},this.performScroll),"function"==typeof(e=this.adapter).visitRendered&&e.visitRendered(this),this.complete())}):void 0},r.prototype.followRedirect=function(){return this.redirectedToLocation&&!this.followedRedirect?(this.location=this.redirectedToLocation,this.controller.replaceHistoryWithLocationAndRestorationIdentifier(this.redirectedToLocation,this.restorationIdentifier),this.followedRedirect=!0):void 0},r.prototype.requestStarted=function(){var t;return this.recordTimingMetric("requestStart"),"function"==typeof(t=this.adapter).visitRequestStarted?t.visitRequestStarted(this):void 0},r.prototype.requestProgressed=function(t){var e;return this.progress=t,"function"==typeof(e=this.adapter).visitRequestProgressed?e.visitRequestProgressed(this):void 0},r.prototype.requestCompletedWithResponse=function(t,r){return this.response=t,null!=r&&(this.redirectedToLocation=e.Location.wrap(r)),this.adapter.visitRequestCompleted(this)},r.prototype.requestFailedWithStatusCode=function(t,e){return this.response=e,this.adapter.visitRequestFailedWithStatusCode(this,t)},r.prototype.requestFinished=function(){var t;return this.recordTimingMetric("requestEnd"),"function"==typeof(t=this.adapter).visitRequestFinished?t.visitRequestFinished(this):void 0},r.prototype.performScroll=function(){return this.scrolled?void 0:("restore"===this.action?this.scrollToRestoredPosition()||this.scrollToTop():this.scrollToAnchor()||this.scrollToTop(),this.scrolled=!0)},r.prototype.scrollToRestoredPosition=function(){var t,e;return t=null!=(e=this.restorationData)?e.scrollPosition:void 0,null!=t?(this.controller.scrollToPosition(t),!0):void 0},r.prototype.scrollToAnchor=function(){return null!=this.location.anchor?(this.controller.scrollToAnchor(this.location.anchor),!0):void 0},r.prototype.scrollToTop=function(){return this.controller.scrollToPosition({x:0,y:0})},r.prototype.recordTimingMetric=function(t){var e;return null!=(e=this.timingMetrics)[t]?e[t]:e[t]=(new Date).getTime()},r.prototype.getTimingMetrics=function(){return e.copyObject(this.timingMetrics)},n=function(t){switch(t){case"replace":return"replaceHistoryWithLocationAndRestorationIdentifier";case"advance":case"restore":return"pushHistoryWithLocationAndRestorationIdentifier"}},r.prototype.shouldIssueRequest=function(){return"restore"===this.action?!this.hasCachedSnapshot():!0},r.prototype.cacheSnapshot=function(){return this.snapshotCached?void 0:(this.controller.cacheSnapshot(),this.snapshotCached=!0)},r.prototype.render=function(t){return this.cancelRender(),this.frame=requestAnimationFrame(function(e){return function(){return e.frame=null,t.call(e)}}(this))},r.prototype.cancelRender=function(){return this.frame?cancelAnimationFrame(this.frame):void 0},r}()}.call(this),function(){var t=function(t,e){return function(){return t.apply(e,arguments)}};e.Controller=function(){function r(){this.clickBubbled=t(this.clickBubbled,this),this.clickCaptured=t(this.clickCaptured,this),this.pageLoaded=t(this.pageLoaded,this),this.history=new e.History(this),this.view=new e.View(this),this.scrollManager=new e.ScrollManager(this),this.restorationData={},this.clearCache(),this.setProgressBarDelay(500)}return r.prototype.start=function(){return e.supported&&!this.started?(addEventListener("click",this.clickCaptured,!0),addEventListener("DOMContentLoaded",this.pageLoaded,!1),this.scrollManager.start(),this.startHistory(),this.started=!0,this.enabled=!0):void 0},r.prototype.disable=function(){return this.enabled=!1},r.prototype.stop=function(){return this.started?(removeEventListener("click",this.clickCaptured,!0),removeEventListener("DOMContentLoaded",this.pageLoaded,!1),this.scrollManager.stop(),this.stopHistory(),this.started=!1):void 0},r.prototype.clearCache=function(){return this.cache=new e.SnapshotCache(10)},r.prototype.visit=function(t,r){var n,o;return null==r&&(r={}),t=e.Location.wrap(t),this.applicationAllowsVisitingLocation(t)?this.locationIsVisitable(t)?(n=null!=(o=r.action)?o:"advance",this.adapter.visitProposedToLocationWithAction(t,n)):window.location=t:void 0},r.prototype.startVisitToLocationWithAction=function(t,r,n){var o;return e.supported?(o=this.getRestorationDataForIdentifier(n),this.startVisit(t,r,{restorationData:o})):window.location=t},r.prototype.setProgressBarDelay=function(t){return this.progressBarDelay=t},r.prototype.startHistory=function(){return this.location=e.Location.wrap(window.location),this.restorationIdentifier=e.uuid(),this.history.start(),this.history.replace(this.location,this.restorationIdentifier)},r.prototype.stopHistory=function(){return this.history.stop()},r.prototype.pushHistoryWithLocationAndRestorationIdentifier=function(t,r){return this.restorationIdentifier=r,this.location=e.Location.wrap(t),this.history.push(this.location,this.restorationIdentifier)},r.prototype.replaceHistoryWithLocationAndRestorationIdentifier=function(t,r){return this.restorationIdentifier=r,this.location=e.Location.wrap(t),this.history.replace(this.location,this.restorationIdentifier)},r.prototype.historyPoppedToLocationWithRestorationIdentifier=function(t,r){var n;return this.restorationIdentifier=r,this.enabled?(n=this.getRestorationDataForIdentifier(this.restorationIdentifier),this.startVisit(t,"restore",{restorationIdentifier:this.restorationIdentifier,restorationData:n,historyChanged:!0}),this.location=e.Location.wrap(t)):this.adapter.pageInvalidated()},r.prototype.getCachedSnapshotForLocation=function(t){var e;return null!=(e=this.cache.get(t))?e.clone():void 0},r.prototype.shouldCacheSnapshot=function(){return this.view.getSnapshot().isCacheable();
+},r.prototype.cacheSnapshot=function(){var t,r;return this.shouldCacheSnapshot()?(this.notifyApplicationBeforeCachingSnapshot(),r=this.view.getSnapshot(),t=this.lastRenderedLocation,e.defer(function(e){return function(){return e.cache.put(t,r.clone())}}(this))):void 0},r.prototype.scrollToAnchor=function(t){var e;return(e=this.view.getElementForAnchor(t))?this.scrollToElement(e):this.scrollToPosition({x:0,y:0})},r.prototype.scrollToElement=function(t){return this.scrollManager.scrollToElement(t)},r.prototype.scrollToPosition=function(t){return this.scrollManager.scrollToPosition(t)},r.prototype.scrollPositionChanged=function(t){var e;return e=this.getCurrentRestorationData(),e.scrollPosition=t},r.prototype.render=function(t,e){return this.view.render(t,e)},r.prototype.viewInvalidated=function(){return this.adapter.pageInvalidated()},r.prototype.viewWillRender=function(t){return this.notifyApplicationBeforeRender(t)},r.prototype.viewRendered=function(){return this.lastRenderedLocation=this.currentVisit.location,this.notifyApplicationAfterRender()},r.prototype.pageLoaded=function(){return this.lastRenderedLocation=this.location,this.notifyApplicationAfterPageLoad()},r.prototype.clickCaptured=function(){return removeEventListener("click",this.clickBubbled,!1),addEventListener("click",this.clickBubbled,!1)},r.prototype.clickBubbled=function(t){var e,r,n;return this.enabled&&this.clickEventIsSignificant(t)&&(r=this.getVisitableLinkForNode(t.target))&&(n=this.getVisitableLocationForLink(r))&&this.applicationAllowsFollowingLinkToLocation(r,n)?(t.preventDefault(),e=this.getActionForLink(r),this.visit(n,{action:e})):void 0},r.prototype.applicationAllowsFollowingLinkToLocation=function(t,e){var r;return r=this.notifyApplicationAfterClickingLinkToLocation(t,e),!r.defaultPrevented},r.prototype.applicationAllowsVisitingLocation=function(t){var e;return e=this.notifyApplicationBeforeVisitingLocation(t),!e.defaultPrevented},r.prototype.notifyApplicationAfterClickingLinkToLocation=function(t,r){return e.dispatch("turbolinks:click",{target:t,data:{url:r.absoluteURL},cancelable:!0})},r.prototype.notifyApplicationBeforeVisitingLocation=function(t){return e.dispatch("turbolinks:before-visit",{data:{url:t.absoluteURL},cancelable:!0})},r.prototype.notifyApplicationAfterVisitingLocation=function(t){return e.dispatch("turbolinks:visit",{data:{url:t.absoluteURL}})},r.prototype.notifyApplicationBeforeCachingSnapshot=function(){return e.dispatch("turbolinks:before-cache")},r.prototype.notifyApplicationBeforeRender=function(t){return e.dispatch("turbolinks:before-render",{data:{newBody:t}})},r.prototype.notifyApplicationAfterRender=function(){return e.dispatch("turbolinks:render")},r.prototype.notifyApplicationAfterPageLoad=function(t){return null==t&&(t={}),e.dispatch("turbolinks:load",{data:{url:this.location.absoluteURL,timing:t}})},r.prototype.startVisit=function(t,e,r){var n;return null!=(n=this.currentVisit)&&n.cancel(),this.currentVisit=this.createVisit(t,e,r),this.currentVisit.start(),this.notifyApplicationAfterVisitingLocation(t)},r.prototype.createVisit=function(t,r,n){var o,i,s,a,u;return i=null!=n?n:{},a=i.restorationIdentifier,s=i.restorationData,o=i.historyChanged,u=new e.Visit(this,t,r),u.restorationIdentifier=null!=a?a:e.uuid(),u.restorationData=e.copyObject(s),u.historyChanged=o,u.referrer=this.location,u},r.prototype.visitCompleted=function(t){return this.notifyApplicationAfterPageLoad(t.getTimingMetrics())},r.prototype.clickEventIsSignificant=function(t){return!(t.defaultPrevented||t.target.isContentEditable||t.which>1||t.altKey||t.ctrlKey||t.metaKey||t.shiftKey)},r.prototype.getVisitableLinkForNode=function(t){return this.nodeIsVisitable(t)?e.closest(t,"a[href]:not([target]):not([download])"):void 0},r.prototype.getVisitableLocationForLink=function(t){var r;return r=new e.Location(t.getAttribute("href")),this.locationIsVisitable(r)?r:void 0},r.prototype.getActionForLink=function(t){var e;return null!=(e=t.getAttribute("data-turbolinks-action"))?e:"advance"},r.prototype.nodeIsVisitable=function(t){var r;return(r=e.closest(t,"[data-turbolinks]"))?"false"!==r.getAttribute("data-turbolinks"):!0},r.prototype.locationIsVisitable=function(t){return t.isPrefixedBy(this.view.getRootLocation())&&t.isHTML()},r.prototype.getCurrentRestorationData=function(){return this.getRestorationDataForIdentifier(this.restorationIdentifier)},r.prototype.getRestorationDataForIdentifier=function(t){var e;return null!=(e=this.restorationData)[t]?e[t]:e[t]={}},r}()}.call(this),function(){!function(){var t,e;if((t=e=document.currentScript)&&!e.hasAttribute("data-turbolinks-suppress-warning"))for(;t=t.parentNode;)if(t===document.body)return console.warn("You are loading Turbolinks from a