From 91f241c5ccbbe75fde16a6eeb257a0a46eb264b7 Mon Sep 17 00:00:00 2001 From: Alex Ma Date: Tue, 10 Nov 2020 16:02:06 -0800 Subject: [PATCH 1/4] added sql-hybrid-cloud-toolkit --- .../Components/ADP/.gitattributes | 63 ++ .../Components/ADP/ADPControl.ipynb | 161 +++++ .../Components/ADP/ADPControl.sln | 37 + .../ADP/ADPControl/ADPControl.csproj | 26 + .../AzureResourceManagerActivity.cs | 244 +++++++ .../ADP/ADPControl/BatchActivity.cs | 214 ++++++ .../Components/ADP/ADPControl/HttpSurface.cs | 103 +++ .../Components/ADP/ADPControl/Orchestrator.cs | 104 +++ .../ADP/ADPControl/StorageActivity.cs | 79 +++ .../Components/ADP/ADPControl/host.json | 16 + .../Components/ADP/BatchWrapper/ActionType.cs | 12 + .../Components/ADP/BatchWrapper/App.config | 22 + .../ADP/BatchWrapper/BatchWrapper.csproj | 98 +++ .../Components/ADP/BatchWrapper/Constants.cs | 34 + .../Components/ADP/BatchWrapper/Payload.cs | 38 ++ .../Components/ADP/BatchWrapper/Program.cs | 178 +++++ .../ADP/BatchWrapper/packages.config | 10 + .../ADP/SqlPackageWrapper/.vscode/launch.json | 27 + .../ADP/SqlPackageWrapper/.vscode/tasks.json | 42 ++ .../ADP/SqlPackageWrapper/ActionType.cs | 12 + .../ADP/SqlPackageWrapper/Constants.cs | 39 ++ .../ADP/SqlPackageWrapper/Payload.cs | 38 ++ .../ADP/SqlPackageWrapper/Program.cs | 162 +++++ .../SqlPackageWrapper.csproj | 14 + SQL-Hybrid-Cloud-Toolkit/Components/readme.md | 1 + SQL-Hybrid-Cloud-Toolkit/README.md | 4 + SQL-Hybrid-Cloud-Toolkit/_config.yml | 2 + SQL-Hybrid-Cloud-Toolkit/_data/toc.yml | 80 +++ .../assets/css/styles.scss | 4 + .../assets/custom/custom.css | 4 + .../assets/custom/custom.js | 1 + .../assets/html/index.html | 25 + .../assets/html/search_form.html | 15 + .../assets/images/copy-button.svg | 1 + .../assets/images/edit-button.svg | 81 +++ .../assets/images/logo_binder.svg | 19 + .../assets/images/logo_jupyterhub.svg | 1 + .../assets/images/sqlserver.png | Bin 0 -> 6665 bytes .../assets/js/anchor.min.js | 9 + SQL-Hybrid-Cloud-Toolkit/assets/js/ga.js | 58 ++ .../assets/js/lunr/lunr.min.js | 1 + SQL-Hybrid-Cloud-Toolkit/assets/js/scripts.js | 150 ++++ .../assets/js/tocbot.min.js | 1 + .../assets/js/turbolinks.js | 6 + .../content/Assessments/CMS.png | Bin 0 -> 62107 bytes .../compatibility-assessment.ipynb | 96 +++ .../content/Assessments/readme.md | 10 + .../Assessments/sql-server-assessment.ipynb | 207 ++++++ .../content/ConnectionDialogue.ipynb | 78 +++ SQL-Hybrid-Cloud-Toolkit/content/LICENSE.md | 5 + .../content/appendices.md | 35 + .../data-portability/VisualBootstrapperNB.PNG | Bin 0 -> 125871 bytes .../data-portability/export-sql-server.ipynb | 560 +++++++++++++++ .../data-portability/import-sql-server.ipynb | 530 +++++++++++++++ .../content/data-portability/readme.md | 20 + .../content/data-portability/setup-adp.ipynb | 643 ++++++++++++++++++ SQL-Hybrid-Cloud-Toolkit/content/glossary.md | 37 + .../content/hadr/add-passive-secondary.ipynb | 39 ++ .../content/hadr/backup-to-blob.ipynb | 369 ++++++++++ .../content/hadr/readme.md | 10 + .../networking/download-VpnClient.ipynb | 168 +++++ .../content/networking/p2svnet-creation.ipynb | 225 ++++++ .../content/networking/readme.md | 13 + .../content/networking/s2svnet-creation.ipynb | 372 ++++++++++ .../content/offline-migration/db-to-MI.ipynb | 39 ++ .../offline-migration/db-to-SQLDB.ipynb | 339 +++++++++ .../content/offline-migration/db-to-VM.ipynb | 271 ++++++++ .../offline-migration/instance-to-MI.ipynb | 34 + .../offline-migration/instance-to-VM.ipynb | 262 +++++++ .../content/offline-migration/readme.md | 16 + .../content/prereqs.ipynb | 201 ++++++ .../provisioning/create-sqldb-azCli.ipynb | 239 +++++++ .../content/provisioning/create-sqldb.ipynb | 243 +++++++ .../content/provisioning/create-sqlmi.ipynb | 339 +++++++++ .../provisioning/create-sqlvm-azCli.ipynb | 557 +++++++++++++++ .../content/provisioning/create-sqlvm.ipynb | 383 +++++++++++ .../content/provisioning/readme.md | 9 + SQL-Hybrid-Cloud-Toolkit/content/readme.md | 35 + SQL-Hybrid-Cloud-Toolkit/requirements.txt | 2 + 79 files changed, 8622 insertions(+) create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/ADP/.gitattributes create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl.ipynb create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl.sln create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl/ADPControl.csproj create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl/AzureResourceManagerActivity.cs create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl/BatchActivity.cs create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl/HttpSurface.cs create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl/Orchestrator.cs create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl/StorageActivity.cs create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl/host.json create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/ADP/BatchWrapper/ActionType.cs create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/ADP/BatchWrapper/App.config create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/ADP/BatchWrapper/BatchWrapper.csproj create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/ADP/BatchWrapper/Constants.cs create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/ADP/BatchWrapper/Payload.cs create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/ADP/BatchWrapper/Program.cs create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/ADP/BatchWrapper/packages.config create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/ADP/SqlPackageWrapper/.vscode/launch.json create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/ADP/SqlPackageWrapper/.vscode/tasks.json create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/ADP/SqlPackageWrapper/ActionType.cs create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/ADP/SqlPackageWrapper/Constants.cs create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/ADP/SqlPackageWrapper/Payload.cs create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/ADP/SqlPackageWrapper/Program.cs create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/ADP/SqlPackageWrapper/SqlPackageWrapper.csproj create mode 100644 SQL-Hybrid-Cloud-Toolkit/Components/readme.md create mode 100644 SQL-Hybrid-Cloud-Toolkit/README.md create mode 100644 SQL-Hybrid-Cloud-Toolkit/_config.yml create mode 100644 SQL-Hybrid-Cloud-Toolkit/_data/toc.yml create mode 100644 SQL-Hybrid-Cloud-Toolkit/assets/css/styles.scss create mode 100644 SQL-Hybrid-Cloud-Toolkit/assets/custom/custom.css create mode 100644 SQL-Hybrid-Cloud-Toolkit/assets/custom/custom.js create mode 100644 SQL-Hybrid-Cloud-Toolkit/assets/html/index.html create mode 100644 SQL-Hybrid-Cloud-Toolkit/assets/html/search_form.html create mode 100644 SQL-Hybrid-Cloud-Toolkit/assets/images/copy-button.svg create mode 100644 SQL-Hybrid-Cloud-Toolkit/assets/images/edit-button.svg create mode 100644 SQL-Hybrid-Cloud-Toolkit/assets/images/logo_binder.svg create mode 100644 SQL-Hybrid-Cloud-Toolkit/assets/images/logo_jupyterhub.svg create mode 100644 SQL-Hybrid-Cloud-Toolkit/assets/images/sqlserver.png create mode 100644 SQL-Hybrid-Cloud-Toolkit/assets/js/anchor.min.js create mode 100644 SQL-Hybrid-Cloud-Toolkit/assets/js/ga.js create mode 100644 SQL-Hybrid-Cloud-Toolkit/assets/js/lunr/lunr.min.js create mode 100644 SQL-Hybrid-Cloud-Toolkit/assets/js/scripts.js create mode 100644 SQL-Hybrid-Cloud-Toolkit/assets/js/tocbot.min.js create mode 100644 SQL-Hybrid-Cloud-Toolkit/assets/js/turbolinks.js create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/Assessments/CMS.png create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/Assessments/compatibility-assessment.ipynb create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/Assessments/readme.md create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/Assessments/sql-server-assessment.ipynb create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/ConnectionDialogue.ipynb create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/LICENSE.md create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/appendices.md create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/data-portability/VisualBootstrapperNB.PNG create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/data-portability/export-sql-server.ipynb create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/data-portability/import-sql-server.ipynb create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/data-portability/readme.md create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/data-portability/setup-adp.ipynb create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/glossary.md create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/hadr/add-passive-secondary.ipynb create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/hadr/backup-to-blob.ipynb create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/hadr/readme.md create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/networking/download-VpnClient.ipynb create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/networking/p2svnet-creation.ipynb create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/networking/readme.md create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/networking/s2svnet-creation.ipynb create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/offline-migration/db-to-MI.ipynb create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/offline-migration/db-to-SQLDB.ipynb create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/offline-migration/db-to-VM.ipynb create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/offline-migration/instance-to-MI.ipynb create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/offline-migration/instance-to-VM.ipynb create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/offline-migration/readme.md create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/prereqs.ipynb create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/provisioning/create-sqldb-azCli.ipynb create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/provisioning/create-sqldb.ipynb create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/provisioning/create-sqlmi.ipynb create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/provisioning/create-sqlvm-azCli.ipynb create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/provisioning/create-sqlvm.ipynb create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/provisioning/readme.md create mode 100644 SQL-Hybrid-Cloud-Toolkit/content/readme.md create mode 100644 SQL-Hybrid-Cloud-Toolkit/requirements.txt diff --git a/SQL-Hybrid-Cloud-Toolkit/Components/ADP/.gitattributes b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/.gitattributes new file mode 100644 index 00000000..1ff0c423 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl.ipynb b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl.ipynb new file mode 100644 index 00000000..99c3f85e --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/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/SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl.sln b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl.sln new file mode 100644 index 00000000..207c2451 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/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/SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl/ADPControl.csproj b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl/ADPControl.csproj new file mode 100644 index 00000000..730cc4b4 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl/ADPControl.csproj @@ -0,0 +1,26 @@ + + + netcoreapp3.1 + v3 + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + Never + + + \ No newline at end of file diff --git a/SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl/AzureResourceManagerActivity.cs b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl/AzureResourceManagerActivity.cs new file mode 100644 index 00000000..21830d76 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/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/SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl/BatchActivity.cs b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl/BatchActivity.cs new file mode 100644 index 00000000..2b84b66c --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/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/SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl/HttpSurface.cs b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl/HttpSurface.cs new file mode 100644 index 00000000..2dcacb91 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/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/SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl/Orchestrator.cs b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl/Orchestrator.cs new file mode 100644 index 00000000..9794aefb --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/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/SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl/StorageActivity.cs b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl/StorageActivity.cs new file mode 100644 index 00000000..d5679162 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/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/SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl/host.json b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/ADPControl/host.json new file mode 100644 index 00000000..ee986a75 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/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/SQL-Hybrid-Cloud-Toolkit/Components/ADP/BatchWrapper/ActionType.cs b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/BatchWrapper/ActionType.cs new file mode 100644 index 00000000..28602c92 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/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/SQL-Hybrid-Cloud-Toolkit/Components/ADP/BatchWrapper/App.config b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/BatchWrapper/App.config new file mode 100644 index 00000000..3617b3e0 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/BatchWrapper/App.config @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SQL-Hybrid-Cloud-Toolkit/Components/ADP/BatchWrapper/BatchWrapper.csproj b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/BatchWrapper/BatchWrapper.csproj new file mode 100644 index 00000000..bbbcf923 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/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/SQL-Hybrid-Cloud-Toolkit/Components/ADP/BatchWrapper/Constants.cs b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/BatchWrapper/Constants.cs new file mode 100644 index 00000000..ded7e299 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/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/SQL-Hybrid-Cloud-Toolkit/Components/ADP/BatchWrapper/Payload.cs b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/BatchWrapper/Payload.cs new file mode 100644 index 00000000..2a037d85 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/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/SQL-Hybrid-Cloud-Toolkit/Components/ADP/BatchWrapper/Program.cs b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/BatchWrapper/Program.cs new file mode 100644 index 00000000..72237b1a --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/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/SQL-Hybrid-Cloud-Toolkit/Components/ADP/BatchWrapper/packages.config b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/BatchWrapper/packages.config new file mode 100644 index 00000000..f8121f7e --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/BatchWrapper/packages.config @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/SQL-Hybrid-Cloud-Toolkit/Components/ADP/SqlPackageWrapper/.vscode/launch.json b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/SqlPackageWrapper/.vscode/launch.json new file mode 100644 index 00000000..9035a71e --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/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/SQL-Hybrid-Cloud-Toolkit/Components/ADP/SqlPackageWrapper/.vscode/tasks.json b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/SqlPackageWrapper/.vscode/tasks.json new file mode 100644 index 00000000..a6efb6b1 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/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/SQL-Hybrid-Cloud-Toolkit/Components/ADP/SqlPackageWrapper/ActionType.cs b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/SqlPackageWrapper/ActionType.cs new file mode 100644 index 00000000..279ce0f7 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/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/SQL-Hybrid-Cloud-Toolkit/Components/ADP/SqlPackageWrapper/Constants.cs b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/SqlPackageWrapper/Constants.cs new file mode 100644 index 00000000..e94bc9b1 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/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/SQL-Hybrid-Cloud-Toolkit/Components/ADP/SqlPackageWrapper/Payload.cs b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/SqlPackageWrapper/Payload.cs new file mode 100644 index 00000000..170ec9d5 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/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/SQL-Hybrid-Cloud-Toolkit/Components/ADP/SqlPackageWrapper/Program.cs b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/SqlPackageWrapper/Program.cs new file mode 100644 index 00000000..2ee94c25 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/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/SQL-Hybrid-Cloud-Toolkit/Components/ADP/SqlPackageWrapper/SqlPackageWrapper.csproj b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/SqlPackageWrapper/SqlPackageWrapper.csproj new file mode 100644 index 00000000..c7d2d265 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/Components/ADP/SqlPackageWrapper/SqlPackageWrapper.csproj @@ -0,0 +1,14 @@ + + + + Exe + netcoreapp2.1;net452 + + + + + + + + + diff --git a/SQL-Hybrid-Cloud-Toolkit/Components/readme.md b/SQL-Hybrid-Cloud-Toolkit/Components/readme.md new file mode 100644 index 00000000..87fe8db9 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/Components/readme.md @@ -0,0 +1 @@ +Folder for helper components used by notebooks in the Hybrid Cloud Toolkit diff --git a/SQL-Hybrid-Cloud-Toolkit/README.md b/SQL-Hybrid-Cloud-Toolkit/README.md new file mode 100644 index 00000000..4ec79845 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/README.md @@ -0,0 +1,4 @@ +# Azure SQL Hybrid Cloud Toolkit +This Jupyter book contains utilities designed to manage a SQL hybrid cloud environment. + +* [Cloud Toolkit Readme](content/readme.md) - Information on individual notebooks in this toolkit. diff --git a/SQL-Hybrid-Cloud-Toolkit/_config.yml b/SQL-Hybrid-Cloud-Toolkit/_config.yml new file mode 100644 index 00000000..39f1cbac --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/_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/SQL-Hybrid-Cloud-Toolkit/_data/toc.yml b/SQL-Hybrid-Cloud-Toolkit/_data/toc.yml new file mode 100644 index 00000000..d93bda12 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/_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: Assessment + url: /Assessments/readme + not_numbered: true + expand_sections: false + sections: + - title: SQL Server Assessment Tool + url: Assessments/sql-server-assessment + - title: Compatibility Assessment + url: Assessments/compatibility-assessment +- 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: 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/SQL-Hybrid-Cloud-Toolkit/assets/css/styles.scss b/SQL-Hybrid-Cloud-Toolkit/assets/css/styles.scss new file mode 100644 index 00000000..da50df3a --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/assets/css/styles.scss @@ -0,0 +1,4 @@ +--- +--- + +@import 'main'; diff --git a/SQL-Hybrid-Cloud-Toolkit/assets/custom/custom.css b/SQL-Hybrid-Cloud-Toolkit/assets/custom/custom.css new file mode 100644 index 00000000..30948f97 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/assets/custom/custom.css @@ -0,0 +1,4 @@ +/* Put your custom CSS here */ +.left { + margin-left: 0px; +} diff --git a/SQL-Hybrid-Cloud-Toolkit/assets/custom/custom.js b/SQL-Hybrid-Cloud-Toolkit/assets/custom/custom.js new file mode 100644 index 00000000..792c8739 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/assets/custom/custom.js @@ -0,0 +1 @@ +// Put your custom javascript here \ No newline at end of file diff --git a/SQL-Hybrid-Cloud-Toolkit/assets/html/index.html b/SQL-Hybrid-Cloud-Toolkit/assets/html/index.html new file mode 100644 index 00000000..82af59f9 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/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/SQL-Hybrid-Cloud-Toolkit/assets/html/search_form.html b/SQL-Hybrid-Cloud-Toolkit/assets/html/search_form.html new file mode 100644 index 00000000..b25a8de7 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/assets/html/search_form.html @@ -0,0 +1,15 @@ +--- +permalink: /search +title: "Search the site" +search_page: true +--- + +
+ +
+
+ + \ No newline at end of file diff --git a/SQL-Hybrid-Cloud-Toolkit/assets/images/copy-button.svg b/SQL-Hybrid-Cloud-Toolkit/assets/images/copy-button.svg new file mode 100644 index 00000000..7cefcfff --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/assets/images/copy-button.svg @@ -0,0 +1 @@ + diff --git a/SQL-Hybrid-Cloud-Toolkit/assets/images/edit-button.svg b/SQL-Hybrid-Cloud-Toolkit/assets/images/edit-button.svg new file mode 100644 index 00000000..8128691b --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/assets/images/edit-button.svg @@ -0,0 +1,81 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/SQL-Hybrid-Cloud-Toolkit/assets/images/logo_binder.svg b/SQL-Hybrid-Cloud-Toolkit/assets/images/logo_binder.svg new file mode 100644 index 00000000..45fecf75 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/assets/images/logo_binder.svg @@ -0,0 +1,19 @@ + + + + +logo + + + + + + + + diff --git a/SQL-Hybrid-Cloud-Toolkit/assets/images/logo_jupyterhub.svg b/SQL-Hybrid-Cloud-Toolkit/assets/images/logo_jupyterhub.svg new file mode 100644 index 00000000..c584badf --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/assets/images/logo_jupyterhub.svg @@ -0,0 +1 @@ +logo_jupyterhubHub \ No newline at end of file diff --git a/SQL-Hybrid-Cloud-Toolkit/assets/images/sqlserver.png b/SQL-Hybrid-Cloud-Toolkit/assets/images/sqlserver.png new file mode 100644 index 0000000000000000000000000000000000000000..cb0062e2ad019cc485b7b3009b66d27e394fa0cd GIT binary patch literal 6665 zcmV+k8usOhP)VN6rlhB2><{R3kzpuWssby^78WQ>+Gcf0Qvd(;^N}2t*zSJ z-yRtm6%P-#wY8s~o~8f|rw$ctYHFwz8>kc-3;+PRxVVUwqcbotTv}Sg!^7dezm1HH z)YQ~;a&jywDbbmlqM@NnNlBHIlzn`BhJ}UIqonlc=sGtytST<;+1W%wLa{$Wyklj{ zi;U;Y%u-NL+OM#-Pg0npuguHKuK)nUc6hqM$i8Q3$by8cCo8l`OsKKD!pP6l008mf z;jcD1xFRInEhxkQ0Js1Ey%-tAdVJWx!?0Oi)?Hi98yCi2U&%T-yAu@IgoM96KJ1>I z)^>H(R8rU;7u#%W%8HAqg^isI8O*P;;E<5>yt>s!McyqZ5iO^|000=6Nkl7(e4 z=V==%i>iDxt~Vk}QIu~+cs#Fv?Kyl60o?gG&wTA^e0kBc`7DS3^Iv-+Us!ks6fR!o z`PZGwmlZJdtmER@@5ka%;aN%3neWBo!NcnuQij>#;)xjDZ@ z)#kJGHlE4b_%nb%Grwx*CpYKkgpLdB^it2*`p?g=+DoW(@iOjWC*gWLlZ7S7hJ0)u zBZf|rn2H55bxBwc!p_Bt6FJtvm+17^k z+e=Nw*L#}e;&uqc5Ma&|{E0RkXz#`AyM%fm{NC#W3gN%1tRyM~-#ly1L&I7(4CP9t zm)n|pW*wbO{O9dmsJmz8n7ltxHnj05`jn@sz3-C6pQ+lmeeg{4s&#m+86)M$K>oL@ zs_6SFp#Rcy{j=pv?HZR@&1p^NSl4xbkmf|wj^O|CHN72-4pjrtR}A%Rs6<+ft4cyS zC`-tdk?Lx+xvOf2g7?|}pztQ3N6S}tBdz+8k*G|{sz=Ypk@adgd1G9Op`q&#Abd1N z;#X0PqX>IF25!@%z2+OiZU=vTGks%6`%BGd-h*xh4c}BRBMTnXo=6zi+io%+zWFQu z_wix+aP5#dLkoU%c&L@p?k}U7?GSnbh`agTO4v<=zW45N`XKo{GQXe^L{1} z5xFcUpjt^_!dA!IpyuYokIK*Y)B7JKWnLTGVV}Jl9n#7fiAYBKp<#B94pgHolt1Yn zefa68$LamQq;vLEHBi9rZ6B(dcGcajz(*@KDU6n=24prqyiBM_)>b}D-%Y2}A3gqi zVR{+fV02+<8t|F{hJJpbRubk_1grZ)+nD#dSE~6AWIBD6b*>CFQ?mEHqlpS~L7{;g zNQC!xRZx7WPkhtTg#yl&yDlOB;fG%zKe~FqC_5dE;U%_k)I*yBU05Nmt5p(C6%ZL| zsu$9m4*>n~2asRi{>@5lcUoDTF???rrm2oc-2JR!y}Fy23G@0(&G}D~JJp?W_YFGw zZ*PA^@Q?Qb{CGu$GX3soMJ)q)-If|Q167LrmujLsiB^i&RVRjB7}H<6AMO?Y@mB;t z{qXQJSB7e3tTeAxbHy+&y{(E#IU&6p8kO=@w6JR{){AQs*v!w5f5+DdehR_<&1h1o zj7?*utRDEQl{3>r{*}S5xw0E6?1u1HzFWqaHGTXj4uAahaSA~XGhPJ}{#l?mHB!rm zzC(vb`QUR;_s^6ofKJ^<%;V`YX{H#IdfCA@Y9<%v~({O!ZP#S4t^ zkHBwM4)~#p*u6BhsaDKcv3#UOh1v|%1H-Pk`NIRT_kaBX!0&(Vet3twl1=Q#s7Id9 zB!ab^BcrI$`Js_e?WuM@OyTF=zu*4x2lCj5cW-D!zlO40Zb(3}@>GR>q(x(oZlt=s zxi=5nivHW%AO3jz8%+N1)2ZE@>1qjgfgr?%g))9O937pFccF}*L?g)tUBid`uDSjG8Y_5B+r5a;w)6^im?k(~Zj^uiH(+0ccgi4@>Qu@-xBMAmp@RQ3?w00x!R7_qg$mA|fsM{W@^d`R5GItQrIz-t9Z^hw0SsyS#asppG>*?7I<% z4WeuGFwNaAZbGOIo?oU9UjMz@9#g9xU{8LJu#ro0L_B3gzMfdIOYNUOJjgVadh{!_ zJ9XBKOlk6raO$WYLd9CC3?Rx2T-y&1j{bS`!m!kRJqK3q$|WcrA0TR0pc)yf2C=%m zH#U{g@fnIT2DLePddQW5c7?5lE*I6vn3@l|gs%MjXUw&1mMPCKDy)jd}&<4=yo{9XM`s9qjD9XH;1OK48gxEh?_RncsxEYvCS54 zB#a8!_JL-Xn3tMzxjXzF<67A;&a5H@pwHVg;!u2`Z@b8qrrK2#7H-$Q=MVZwMIy&1 z(u1L0ps7&A85jTXV4XMUP^ST5)7^gK-h0(V+Avk&RaqDk7%oX&ecBbeDSgH>ol~pQ+dgN4Ux|_whuzG!t68oMuhC~JdZg@x@el72u z>oQKV`ZR@^W@(o43T*xSzVg8FK>tKp;2OBMUP< z^ysSE2OZ#743&>3e zxLl4p{Rr3e9U4Ce(DekQadX>+Av079yW{G5#^LbMhdwYXrqPY-axjW zd*c18N&>U3S+9f3yw(LDRWg@dy!9R+OBW99;^y{{i;>F|K8m?5vv%V@6!(cflduAM z9E@gK*Vonpv)03LhlevTjF9{v1zB7R{`oW>id&g!*wKW3vv%S;;&*R(C=_BuwVqvD zlk*ynXR{ec|UjFw>=d_8k+~``J1)1 zH5ayme=yc=!FjbTzXeNoAF7C=tKTdxE&^=d7hB}BZp|ulH!r1+_|qgOfIM)}{j10# z+S$i#xbxAST3cKUP`S))Ti#c6U(Vd7M_cSQINF!?RAm@ag!upA8`I&rM`dTLmE`I4v z-1CK35w&-e8ykCj&BB+xh3fS-E0roU| z8OXWlPU(@hcM@Uri@}zBs9L$R*%)=Dqr@%6`y8ET0=`V~-Q9xwGSmCKl3yh7-u>om zCXm&x9y1Up)ro=^P+ST{T5Y^ zv<6V+MY9T9?g7{+@p=1_;>7W~T}$ zqyM?S*shf7Y+5g~Y-jlsufj_Bv&U%Wjnl>}Q!*DzkU(rj(dc-=SBnl+r=y%job~ z#hlbtHUNN}XUoe_r0w+z0wq?FoG;NJjUsiB^G z@WO#!WwwLu6C67p0M0fO*|2T-2@To4PU}@|G$QeBqX@mqKuM{V1lpKK}!&-_uX9k0&T2?9>VPeaDZRLiZ!pd0=%;|b#6B1M_$(rzX9$3jM4Tnm zY&^zx>I}@gR;Sb2jO`z@6s*T7J6>iK;*MFYzA8WqYv7D+^uGdzPn4`kQG%ycPu+n= zt5xMl$gTI)%}sG?6)2lH)GoDhR+tyrEMD4-l-~a<|CR4f^y=pF#{T}sa;usR8_zCn zuGe9Fp|un;-flErRHmNx`U~~?LcetS&9ofbb;i=k0!T8=Sm)F?oZU*}c#_6btW)x5 zZ}>Q4p-U!T1xVm(F$q#kLAv-h!r?MY#w1}m&uF&J((-Vukg|!5Eer7Z=b-ml(Q}if zKN-X)NsW*tK^3Vqu0hH(t4*;k1yDE0s#v5{%mefV(Dzf~Dyykfv4GIOg@S$qwNYdX zrD`L$xxiS_qmVj_(eQq2>9l>?=mDF-nHQMs70*MjL!KXJ6V6Xzb8x$k?BSzC<@!aQYXGg>L~mS)a#ub+^y3fM#0k_)zr(cDE7p zEOfjH_5)nWeEl$Y3NQ-CGX}{biW`CBZ9HDsciuk-oo*7m%vY8cc)BQEf}$hBupnuO}Tu7e16>Pb8tDJoX%P zyin>tn&#(_+HB~k+2pJ+&|8%TUjqHi4&LvtnPnTCKip)A_X2UtZtJ&6rHK zn#)1{%AcNs`z;h^x*;icR#%xb$r+x5zESWd!@zuzNEeIgRhBNfN0&M~I~|xP?<1Ll zN!B`@&Z^)4)?=NHJrCXjS_WJh=-0kPdIao`b##_EV_M8s!TTk(PL3j=_v`N;XAo6YjN{q66L!!%697`vWfHE--h$I9(v4 zTNh-krZkkTM!qVvR}3?2TohReKFQ&5>t)d6c$4%(_+U0o8 zAz1+E*^nHGCs_uyE{NxjZ$(ZFi zEwaG{7kUBsBx|8wun~+$Ry`JvhOkQeeWXI0mS%{6+x9~XdR!>(P~gf_e?cUt9WprP zZ>2sLmZkCMq4QXhjCs>Pu#RcOjVKM~##RIhDwTq;9tb~}g-AbH=B+C*6xq%}&e#ms zB-Es8T0##v%qrHYX@8|DOGC6!lF}@mg)V8}MbIJlBt`A-S{D!UJoj1z-8W<3q;Ta3 zFdBX^3J8NB=(C~lWlDOWc>4%J4=Y`4A?UBrItCxhx}30rT1;B|x!I+GAg~L*W_wfN)PCMjU#hC3%0Tc4ce&H2i&XPufZnC;>8U0oe`Em0OYp zsgeMP(9v9Ho1$3oNT+8YL zbcl9&WHn|RF=g?d@8cM~>|5z}kR!*56}%Tp#yrrUn-?(wixT&P#^{0Y8a5{j-^T?` zk%xPl;I6pP11^e{blPGERDUQA^bmOkbY&wMlLPE?9Def?phJ{M+7{ReDhq|phEB$4 z*(Jm_Yl6gt(jgam1#~)1I9U7X5PvKcqOXKbzOsswt^or-?v(;^>Oc?dR4h4G-e!(R zBJ^~ry5yJKYoKFpu?QfOXmCJ?!WWfKf-YirU7;*7yImO$9j7Je9(ZKZqWeT`W;XxQ z0rC(oZeT}ex4k(a#35{VcrA2PJLOzA3Wo^Qqrv_d#UN{@HeAez#jU`pmLT-i7fj1- z)`_-j_*#8 zy&8JT@)nHD$r&w-Sq85_C4$BX&XRU!K(*8?=X{~B6ECuQNwm|D3?siSFZX^ebccM& z^;JM-K`)^-=xF0^5l1bsSuNBV(EYb9xkaNWreo>g6#Fo?A_V=3W^uaiVbIb|M7{0} zP`X84XFND~9s-X2Pm(b?7sP;_DszT_+mNzwT zN--jxWObJp68}hApWhN2z3#c$E!KGkI$oe4E45 zf^-*rjl6{rF7AU)YeUav_-`T}UP#k61EbD!?K$YJv}g4!>`7`#&`reCAJ1sL<9kkq z?f8ARQS&}(6DzWsuc~<#y5}((|4nk0 zFVX1Ede8GK?Rwys9_691fp~6xzth=RpSf`vdbMZIXxE78y@s{<@9{~$F8fW$F?r3r z{a*RY1Rpdj6u+6LJin;aF!>a|OYHjU_luoU>UWC0@f~CDeaG0h#70HbbKL&}@Yz-u TF^^F+00000NkvXXu0mjfet1fH literal 0 HcmV?d00001 diff --git a/SQL-Hybrid-Cloud-Toolkit/assets/js/anchor.min.js b/SQL-Hybrid-Cloud-Toolkit/assets/js/anchor.min.js new file mode 100644 index 00000000..e302d89b --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/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/SQL-Hybrid-Cloud-Toolkit/assets/js/ga.js b/SQL-Hybrid-Cloud-Toolkit/assets/js/ga.js new file mode 100644 index 00000000..0dae3ea9 --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/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/SQL-Hybrid-Cloud-Toolkit/assets/js/lunr/lunr.min.js b/SQL-Hybrid-Cloud-Toolkit/assets/js/lunr/lunr.min.js new file mode 100644 index 00000000..f45a81eb --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/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/SQL-Hybrid-Cloud-Toolkit/assets/js/tocbot.min.js b/SQL-Hybrid-Cloud-Toolkit/assets/js/tocbot.min.js new file mode 100644 index 00000000..943d8fdb --- /dev/null +++ b/SQL-Hybrid-Cloud-Toolkit/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