mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 18:46:43 -05:00
Compare commits
127 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15b8fa72ec | ||
|
|
2cf3357301 | ||
|
|
aee8bc2759 | ||
|
|
adf848c1ef | ||
|
|
7ad328ee95 | ||
|
|
2f18753b1f | ||
|
|
e182649adc | ||
|
|
a74119038f | ||
|
|
e169005571 | ||
|
|
b10b52e4fe | ||
|
|
5f04a4d499 | ||
|
|
d6e1e8eb52 | ||
|
|
9977e83380 | ||
|
|
099e94fa2d | ||
|
|
7732f5c0d1 | ||
|
|
151a18e3ee | ||
|
|
e59de59e61 | ||
|
|
f96fd911c1 | ||
|
|
6c89c61b0d | ||
|
|
97a4de4111 | ||
|
|
a70dce7855 | ||
|
|
757ac1d4aa | ||
|
|
4092b6493b | ||
|
|
6349f1bd49 | ||
|
|
0c82024cf3 | ||
|
|
131e0a6b45 | ||
|
|
aeb22011d2 | ||
|
|
3d82074656 | ||
|
|
6dc07e5804 | ||
|
|
89d515d3ae | ||
|
|
9df56c5c0f | ||
|
|
048f85d918 | ||
|
|
4d3443c192 | ||
|
|
f69dc6a445 | ||
|
|
fde031be48 | ||
|
|
08735c9434 | ||
|
|
f748a8c7bb | ||
|
|
67e3d2ebdb | ||
|
|
d17ca1561f | ||
|
|
6f731fcd9e | ||
|
|
d86e1eec10 | ||
|
|
cb567989da | ||
|
|
40675fcadb | ||
|
|
0299ef1d83 | ||
|
|
f544ca3be0 | ||
|
|
273f40e2b3 | ||
|
|
8027993ab4 | ||
|
|
1078d67728 | ||
|
|
593cb45a50 | ||
|
|
0b1239b755 | ||
|
|
a0ef594792 | ||
|
|
e2cf607896 | ||
|
|
f0eeb76846 | ||
|
|
5da30e6111 | ||
|
|
a9eb6880d4 | ||
|
|
426f1ae99b | ||
|
|
64dd0f0cad | ||
|
|
6ac5b7c8a5 | ||
|
|
397354ebc3 | ||
|
|
2a7b90fd70 | ||
|
|
1554e51932 | ||
|
|
d060f1b9a0 | ||
|
|
c8632c255a | ||
|
|
2ac03b9ef4 | ||
|
|
a0d89449cc | ||
|
|
b03a914934 | ||
|
|
d3bcb942f5 | ||
|
|
84822b23ac | ||
|
|
40ca82c63d | ||
|
|
f4a6b42b3a | ||
|
|
7ad631d4a5 | ||
|
|
4b7baa652f | ||
|
|
148e802f4a | ||
|
|
7bb4d00073 | ||
|
|
3b20e8a61c | ||
|
|
6e0a4f27de | ||
|
|
21ddf30a7b | ||
|
|
2ade45858e | ||
|
|
e0b1a3460d | ||
|
|
f44c714cf2 | ||
|
|
f72e12fe32 | ||
|
|
0b6fb504dc | ||
|
|
145b2491df | ||
|
|
aa30b52d03 | ||
|
|
6edcbbb738 | ||
|
|
815c61315c | ||
|
|
172a044ba7 | ||
|
|
2a81a0a70f | ||
|
|
749989cd0b | ||
|
|
8d42182db8 | ||
|
|
bb2a1db6e8 | ||
|
|
c579ecb111 | ||
|
|
175d46d508 | ||
|
|
870ff39527 | ||
|
|
ddd0b8b4bc | ||
|
|
c7cca5afea | ||
|
|
e63e4f0901 | ||
|
|
ddc8c00090 | ||
|
|
34170e7741 | ||
|
|
f5e4b32d01 | ||
|
|
28fef53731 | ||
|
|
438bc67072 | ||
|
|
472c9decfa | ||
|
|
6cd2d6c942 | ||
|
|
39e1181c5d | ||
|
|
c898b50b94 | ||
|
|
271fe62344 | ||
|
|
c18a54bc1d | ||
|
|
b57bf53b67 | ||
|
|
c699179e15 | ||
|
|
690937443c | ||
|
|
698b79f0f3 | ||
|
|
798af5fc2d | ||
|
|
af55dcfb42 | ||
|
|
76781d6cf4 | ||
|
|
99e3da5b48 | ||
|
|
6b657259a5 | ||
|
|
cbe2ba0901 | ||
|
|
b3d99117ca | ||
|
|
32ac586431 | ||
|
|
bd4676ac8c | ||
|
|
536628603e | ||
|
|
ea7fe08b98 | ||
|
|
6c920f6d54 | ||
|
|
a2f7136728 | ||
|
|
a082c1e478 | ||
|
|
32a6385fef |
@@ -12,6 +12,10 @@
|
||||
{
|
||||
"file": "build\\actions\\AutoMerge\\dist\\index.js",
|
||||
"_justification": "False positive from webpacked code"
|
||||
},
|
||||
{
|
||||
"file": ".devcontainer\\devcontainer.json",
|
||||
"_justification": "Local development environment - not used in production"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
2
.github/label-actions.yml
vendored
Normal file
2
.github/label-actions.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
Needs Logs:
|
||||
comment: "We need more info to debug your particular issue. If you could attach your logs to the issue (ensure no private data is in them), it would help us fix the issue much faster.\n\nTo find your logs:\n\n- Open command palette (Click **View** -> **Command Palette**)\n- Run the command: **`Developer: Open Logs Folder`**\n\nThis will open the log file locally. Please zip up this folder and attach it to the issue."
|
||||
15
.github/workflows/on-label.yml
vendored
Normal file
15
.github/workflows/on-label.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: On Label
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
processLabelAction:
|
||||
name: Process Label Action
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Process Label Action
|
||||
uses: hramos/label-actions@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,5 +1,14 @@
|
||||
# Change Log
|
||||
|
||||
## Version 1.25.0
|
||||
* Release date: December 8, 2020
|
||||
* Release status: General Availability
|
||||
* Kusto extension improvements
|
||||
* SQL Project extension improvements
|
||||
* Notebook improvements
|
||||
* Azure Browse Connections Preview performance improvements
|
||||
* Bug Fixes
|
||||
|
||||
## Version 1.24.0
|
||||
* Release date: November 12, 2020
|
||||
* Release status: General Availability
|
||||
|
||||
14
README.md
14
README.md
@@ -131,10 +131,10 @@ Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
Licensed under the [Source EULA](LICENSE.txt).
|
||||
|
||||
[win-user]: https://go.microsoft.com/fwlink/?linkid=2148607
|
||||
[win-system]: https://go.microsoft.com/fwlink/?linkid=2148907
|
||||
[win-zip]: https://go.microsoft.com/fwlink/?linkid=2148908
|
||||
[osx-zip]: https://go.microsoft.com/fwlink/?linkid=2148710
|
||||
[linux-zip]: https://go.microsoft.com/fwlink/?linkid=2148708
|
||||
[linux-rpm]: https://go.microsoft.com/fwlink/?linkid=2148709
|
||||
[linux-deb]: https://go.microsoft.com/fwlink/?linkid=2148806
|
||||
[win-user]: https://go.microsoft.com/fwlink/?linkid=2150927
|
||||
[win-system]: https://go.microsoft.com/fwlink/?linkid=2150928
|
||||
[win-zip]: https://go.microsoft.com/fwlink/?linkid=2151312
|
||||
[osx-zip]: https://go.microsoft.com/fwlink/?linkid=2151311
|
||||
[linux-zip]: https://go.microsoft.com/fwlink/?linkid=2151508
|
||||
[linux-rpm]: https://go.microsoft.com/fwlink/?linkid=2151407
|
||||
[linux-deb]: https://go.microsoft.com/fwlink/?linkid=2151506
|
||||
|
||||
@@ -43,6 +43,7 @@ function createDefaultConfig(quality: string): Config {
|
||||
}
|
||||
|
||||
function getConfig(quality: string): Promise<Config> {
|
||||
console.log(`Getting config for quality ${quality}`);
|
||||
const client = new DocumentClient(process.env['AZURE_DOCUMENTDB_ENDPOINT']!, { masterKey: process.env['AZURE_DOCUMENTDB_MASTERKEY'] });
|
||||
const collection = 'dbs/builds/colls/config';
|
||||
const query = {
|
||||
@@ -52,13 +53,13 @@ function getConfig(quality: string): Promise<Config> {
|
||||
]
|
||||
};
|
||||
|
||||
return new Promise<Config>((c, e) => {
|
||||
return retry(() => new Promise<Config>((c, e) => {
|
||||
client.queryDocuments(collection, query, { enableCrossPartitionQuery: true }).toArray((err, results) => {
|
||||
if (err && err.code !== 409) { return e(err); }
|
||||
|
||||
c(!results || results.length === 0 ? createDefaultConfig(quality) : results[0] as any as Config);
|
||||
});
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
interface Asset {
|
||||
@@ -86,6 +87,7 @@ function createOrUpdate(commit: string, quality: string, platform: string, type:
|
||||
updateTries++;
|
||||
|
||||
return new Promise<void>((c, e) => {
|
||||
console.log(`Querying existing documents to update...`);
|
||||
client.queryDocuments(collection, updateQuery, { enableCrossPartitionQuery: true }).toArray((err, results) => {
|
||||
if (err) { return e(err); }
|
||||
if (results.length !== 1) { return e(new Error('No documents')); }
|
||||
@@ -101,6 +103,7 @@ function createOrUpdate(commit: string, quality: string, platform: string, type:
|
||||
release.updates[platform] = type;
|
||||
}
|
||||
|
||||
console.log(`Replacing existing document with updated version`);
|
||||
client.replaceDocument(release._self, release, err => {
|
||||
if (err && err.code === 409 && updateTries < 5) { return c(update()); }
|
||||
if (err) { return e(err); }
|
||||
@@ -112,7 +115,8 @@ function createOrUpdate(commit: string, quality: string, platform: string, type:
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise<void>((c, e) => {
|
||||
return retry(() => new Promise<void>((c, e) => {
|
||||
console.log(`Attempting to create document`);
|
||||
client.createDocument(collection, release, err => {
|
||||
if (err && err.code === 409) { return c(update()); }
|
||||
if (err) { return e(err); }
|
||||
@@ -120,7 +124,7 @@ function createOrUpdate(commit: string, quality: string, platform: string, type:
|
||||
console.log('Build successfully published.');
|
||||
c();
|
||||
});
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
async function assertContainer(blobService: azure.BlobService, quality: string): Promise<void> {
|
||||
@@ -188,7 +192,6 @@ async function publish(commit: string, quality: string, platform: string, type:
|
||||
console.log(`Blob ${quality}, ${blobName} already exists, not publishing again.`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Uploading blobs to Azure storage...');
|
||||
|
||||
await uploadBlob(blobService, quality, blobName, file);
|
||||
@@ -247,6 +250,22 @@ async function publish(commit: string, quality: string, platform: string, type:
|
||||
await createOrUpdate(commit, quality, platform, type, release, asset, isUpdate);
|
||||
}
|
||||
|
||||
const RETRY_TIMES = 10;
|
||||
async function retry<T>(fn: () => Promise<T>): Promise<T> {
|
||||
for (let run = 1; run <= RETRY_TIMES; run++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (err) {
|
||||
if (!/ECONNRESET/.test(err.message)) {
|
||||
throw err;
|
||||
}
|
||||
console.log(`Caught error ${err} - ${run}/${RETRY_TIMES}`);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Retried too many times');
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const commit = process.env['BUILD_SOURCEVERSION'];
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ steps:
|
||||
set -e
|
||||
APP_ROOT=$(agent.builddirectory)/azuredatastudio-darwin
|
||||
APP_NAME="`ls $APP_ROOT | head -n 1`"
|
||||
yarn smoketest --build "$APP_ROOT/$APP_NAME" --screenshots "$(build.artifactstagingdirectory)/smokeshots"
|
||||
yarn smoketest --build "$APP_ROOT/$APP_NAME" --screenshots "$(build.artifactstagingdirectory)/smokeshots" --log "$(build.artifactstagingdirectory)/logs/darwin/smoke.log"
|
||||
displayName: Run smoke tests (Electron)
|
||||
continueOnError: true
|
||||
condition: and(succeeded(), eq(variables['RUN_TESTS'], 'true'))
|
||||
|
||||
@@ -213,9 +213,9 @@ const externalExtensions = [
|
||||
'arc',
|
||||
'asde-deployment',
|
||||
'azdata',
|
||||
'azurehybridtoolkit',
|
||||
'cms',
|
||||
'dacpac',
|
||||
'data-workspace',
|
||||
'import',
|
||||
'kusto',
|
||||
'liveshare',
|
||||
|
||||
@@ -247,9 +247,9 @@ const externalExtensions = [
|
||||
'arc',
|
||||
'asde-deployment',
|
||||
'azdata',
|
||||
'azurehybridtoolkit',
|
||||
'cms',
|
||||
'dacpac',
|
||||
'data-workspace',
|
||||
'import',
|
||||
'kusto',
|
||||
'liveshare',
|
||||
|
||||
@@ -65,13 +65,7 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"import pandas,sys,os,json,html,getpass,time, tempfile\n",
|
||||
"pandas_version = pandas.__version__.split('.')\n",
|
||||
"pandas_major = int(pandas_version[0])\n",
|
||||
"pandas_minor = int(pandas_version[1])\n",
|
||||
"pandas_patch = int(pandas_version[2])\n",
|
||||
"if not (pandas_major > 0 or (pandas_major == 0 and pandas_minor > 24) or (pandas_major == 0 and pandas_minor == 24 and pandas_patch >= 2)):\n",
|
||||
" sys.exit('Please upgrade the Notebook dependency before you can proceed, you can do it by running the \"Reinstall Notebook dependencies\" command in command palette (View menu -> Command Palette…).')\n",
|
||||
"import sys,os,json,html,getpass,time, tempfile\n",
|
||||
"def run_command(command):\n",
|
||||
" print(\"Executing: \" + command)\n",
|
||||
" !{command}\n",
|
||||
@@ -138,7 +132,13 @@
|
||||
" sys.exit(f'Password is required.')\n",
|
||||
" confirm_password = getpass.getpass(prompt = 'Confirm password')\n",
|
||||
" if arc_admin_password != confirm_password:\n",
|
||||
" sys.exit(f'Passwords do not match.')"
|
||||
" sys.exit(f'Passwords do not match.')\n",
|
||||
"\n",
|
||||
"os.environ[\"SPN_CLIENT_ID\"] = sp_client_id\n",
|
||||
"os.environ[\"SPN_TENANT_ID\"] = sp_tenant_id\n",
|
||||
"if \"AZDATA_NB_VAR_SP_CLIENT_SECRET\" in os.environ:\n",
|
||||
" os.environ[\"SPN_CLIENT_SECRET\"] = os.environ[\"AZDATA_NB_VAR_SP_CLIENT_SECRET\"]\n",
|
||||
"os.environ[\"SPN_AUTHORITY\"] = \"https://login.microsoftonline.com\""
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "e7e10828-6cae-45af-8c2f-1484b6d4f9ac",
|
||||
@@ -188,7 +188,7 @@
|
||||
"os.environ[\"AZDATA_PASSWORD\"] = arc_admin_password\n",
|
||||
"if os.name == 'nt':\n",
|
||||
" print(f'If you don\\'t see output produced by azdata, you can run the following command in a terminal window to check the deployment status:\\n\\t {os.environ[\"AZDATA_NB_VAR_KUBECTL\"]} get pods -n {arc_data_controller_namespace}')\n",
|
||||
"run_command(f'azdata arc dc create --connectivity-mode Indirect -n {arc_data_controller_name} -ns {arc_data_controller_namespace} -s {arc_subscription} -g {arc_resource_group} -l {arc_data_controller_location} -sc {arc_data_controller_storage_class} --profile-name {arc_profile}')\n",
|
||||
"run_command(f'azdata arc dc create --connectivity-mode {arc_data_controller_connectivity_mode} -n {arc_data_controller_name} -ns {arc_data_controller_namespace} -s {arc_subscription} -g {arc_resource_group} -l {arc_data_controller_location} -sc {arc_data_controller_storage_class} --profile-name {arc_profile}')\n",
|
||||
"print(f'Azure Arc Data Controller: {arc_data_controller_name} created.') "
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@@ -114,6 +114,8 @@
|
||||
"# Login to the data controller.\n",
|
||||
"#\n",
|
||||
"os.environ[\"AZDATA_PASSWORD\"] = os.environ[\"AZDATA_NB_VAR_CONTROLLER_PASSWORD\"]\n",
|
||||
"os.environ[\"KUBECONFIG\"] = controller_kubeconfig\n",
|
||||
"os.environ[\"KUBECTL_CONTEXT\"] = controller_kubectl_context\n",
|
||||
"cmd = f'azdata login -e {controller_endpoint} -u {controller_username}'\n",
|
||||
"out=run_command()"
|
||||
],
|
||||
|
||||
@@ -114,6 +114,8 @@
|
||||
"# Login to the data controller.\n",
|
||||
"#\n",
|
||||
"os.environ[\"AZDATA_PASSWORD\"] = os.environ[\"AZDATA_NB_VAR_CONTROLLER_PASSWORD\"]\n",
|
||||
"os.environ[\"KUBECONFIG\"] = controller_kubeconfig\n",
|
||||
"os.environ[\"KUBECTL_CONTEXT\"] = controller_kubectl_context\n",
|
||||
"cmd = f'azdata login -e {controller_endpoint} -u {controller_username}'\n",
|
||||
"out=run_command()"
|
||||
],
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
"name": "arc",
|
||||
"displayName": "%arc.displayName%",
|
||||
"description": "%arc.description%",
|
||||
"version": "0.6.3",
|
||||
"version": "0.7.0",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
||||
"icon": "images/extension.png",
|
||||
"engines": {
|
||||
"vscode": "*",
|
||||
"azdata": ">=1.23.0"
|
||||
"azdata": ">=1.25.0"
|
||||
},
|
||||
"activationEvents": [
|
||||
"onCommand:arc.connectToController",
|
||||
@@ -137,7 +137,11 @@
|
||||
"description": "%resource.type.azure.arc.description%",
|
||||
"platforms": "*",
|
||||
"icon": "./images/data_controller.svg",
|
||||
"tags": ["Hybrid", "SQL Server", "PostgreSQL"],
|
||||
"tags": [
|
||||
"Hybrid",
|
||||
"SQL Server",
|
||||
"PostgreSQL"
|
||||
],
|
||||
"providers": [
|
||||
{
|
||||
"notebookWizard": {
|
||||
@@ -196,7 +200,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "%arc.data.controller.data.controller.create.title%",
|
||||
"title": "%arc.data.controller.create.azureconfig.title%",
|
||||
"sections": [
|
||||
{
|
||||
"title": "%arc.data.controller.project.details.title%",
|
||||
@@ -210,53 +214,14 @@
|
||||
"type": "azure_account",
|
||||
"required": true,
|
||||
"subscriptionVariableName": "AZDATA_NB_VAR_ARC_SUBSCRIPTION",
|
||||
"displaySubscriptionVariableName": "AZDATA_NB_VAR_ARC_DISPLAY_SUBSCRIPTION",
|
||||
"resourceGroupVariableName": "AZDATA_NB_VAR_ARC_RESOURCE_GROUP"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "%arc.data.controller.data.controller.details.title%",
|
||||
"fields": [
|
||||
{
|
||||
"type": "readonly_text",
|
||||
"label": "%arc.data.controller.data.controller.details.description%",
|
||||
"labelWidth": "600px"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "%arc.data.controller.arc.data.controller.namespace%",
|
||||
"textValidationRequired": true,
|
||||
"textValidationRegex": "^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$",
|
||||
"textValidationDescription": "%arc.data.controller.arc.data.controller.namespace.validation.description%",
|
||||
"defaultValue": "arc",
|
||||
"required": true,
|
||||
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAMESPACE"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "%arc.data.controller.arc.data.controller.name%",
|
||||
"textValidationRequired": true,
|
||||
"textValidationRegex": "^[a-z0-9]([-.a-z0-9]{0,251}[a-z0-9])?$",
|
||||
"textValidationDescription": "%arc.data.controller.arc.data.controller.name.validation.description%",
|
||||
"defaultValue": "arc-dc",
|
||||
"required": true,
|
||||
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAME"
|
||||
},
|
||||
{
|
||||
"label": "%arc.storage-class.dc.label%",
|
||||
"description": "%arc.sql.storage-class.dc.description%",
|
||||
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_STORAGE_CLASS",
|
||||
"type": "kube_storage_class",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "azure_locations",
|
||||
"label": "%arc.data.controller.arc.data.controller.location%",
|
||||
"label": "%arc.data.controller.location%",
|
||||
"defaultValue": "eastus",
|
||||
"required": true,
|
||||
"locationVariableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_LOCATION",
|
||||
"displayLocationVariableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_DISPLAY_LOCATION",
|
||||
"locations": [
|
||||
"australiaeast",
|
||||
"centralus",
|
||||
@@ -274,6 +239,141 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "%arc.data.controller.connectivitymode%",
|
||||
"fields": [
|
||||
{
|
||||
"type": "readonly_text",
|
||||
"label": "%arc.data.controller.connectivitymode.description%",
|
||||
"labelWidth": "600px"
|
||||
},
|
||||
{
|
||||
"type": "options",
|
||||
"label": "%arc.data.controller.connectivitymode%",
|
||||
"required": true,
|
||||
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE",
|
||||
"options": {
|
||||
"values": [
|
||||
{
|
||||
"name": "indirect",
|
||||
"displayName": "%arc.data.controller.indirect%"
|
||||
},
|
||||
{
|
||||
"name": "direct",
|
||||
"displayName": "%arc.data.controller.direct%"
|
||||
}
|
||||
],
|
||||
"defaultValue": "%arc.data.controller.indirect%",
|
||||
"optionsType": "radio"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "readonly_text",
|
||||
"label": "%arc.data.controller.serviceprincipal.description%",
|
||||
"labelWidth": "600px",
|
||||
"links": [
|
||||
{
|
||||
"text": "%arc.data.controller.readmore%",
|
||||
"url": "https://docs.microsoft.com/azure/azure-arc/data/upload-metrics"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "%arc.data.controller.spclientid%",
|
||||
"description": "%arc.data.controller.spclientid.description%",
|
||||
"variableName": "AZDATA_NB_VAR_SP_CLIENT_ID",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"defaultValue": "",
|
||||
"placeHolder": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"enabled": {
|
||||
"target": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE",
|
||||
"value": "direct"
|
||||
},
|
||||
"validations" : [{
|
||||
"type": "regex_match",
|
||||
"regex": "^[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}$",
|
||||
"description": "%arc.data.controller.spclientid.validation.description%"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"label": "%arc.data.controller.spclientsecret%",
|
||||
"description": "%arc.data.controller.spclientsecret.description%",
|
||||
"variableName": "AZDATA_NB_VAR_SP_CLIENT_SECRET",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"defaultValue": "",
|
||||
"enabled": {
|
||||
"target": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE",
|
||||
"value": "direct"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "%arc.data.controller.sptenantid%",
|
||||
"description": "%arc.data.controller.sptenantid.description%",
|
||||
"variableName": "AZDATA_NB_VAR_SP_TENANT_ID",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"defaultValue": "",
|
||||
"enabled": false,
|
||||
"valueProvider": {
|
||||
"providerId": "subscription-id-to-tenant-id",
|
||||
"triggerField": "AZDATA_NB_VAR_ARC_SUBSCRIPTION"
|
||||
},
|
||||
"validations" : [{
|
||||
"type": "regex_match",
|
||||
"regex": "^[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}$",
|
||||
"description": "%arc.data.controller.sptenantid.validation.description%"
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "%arc.data.controller.create.controllerconfig.title%",
|
||||
"sections": [
|
||||
{
|
||||
"title": "%arc.data.controller.details.title%",
|
||||
"fields": [
|
||||
{
|
||||
"type": "readonly_text",
|
||||
"label": "%arc.data.controller.details.description%",
|
||||
"labelWidth": "600px"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "%arc.data.controller.namespace%",
|
||||
"validations" : [{
|
||||
"type": "regex_match",
|
||||
"regex": "^[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])?$",
|
||||
"description": "%arc.data.controller.namespace.validation.description%"
|
||||
}],
|
||||
"defaultValue": "arc",
|
||||
"required": true,
|
||||
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAMESPACE"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "%arc.data.controller.name%",
|
||||
"validations" : [{
|
||||
"type": "regex_match",
|
||||
"regex": "^[a-z0-9]([-.a-z0-9]{0,251}[a-z0-9])?$",
|
||||
"description": "%arc.data.controller.name.validation.description%"
|
||||
}],
|
||||
"defaultValue": "arc-dc",
|
||||
"required": true,
|
||||
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAME"
|
||||
},
|
||||
{
|
||||
"label": "%arc.storage-class.dc.label%",
|
||||
"description": "%arc.sql.storage-class.dc.description%",
|
||||
"variableName": "AZDATA_NB_VAR_ARC_DATA_CONTROLLER_STORAGE_CLASS",
|
||||
"type": "kube_storage_class",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "%arc.data.controller.admin.account.title%",
|
||||
"fields": [
|
||||
@@ -300,7 +400,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "%arc.data.controller.data.controller.create.summary.title%",
|
||||
"title": "%arc.data.controller.create.summary.title%",
|
||||
"isSummaryPage": true,
|
||||
"fieldHeight": "16px",
|
||||
"sections": [
|
||||
@@ -454,23 +554,11 @@
|
||||
{
|
||||
"title": "%arc.data.controller.summary.azure%",
|
||||
"fields": [
|
||||
{
|
||||
"label": "%arc.data.controller.summary.data.controller.namespace%",
|
||||
"type": "readonly_text",
|
||||
"isEvaluated": true,
|
||||
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAMESPACE)"
|
||||
},
|
||||
{
|
||||
"label": "%arc.data.controller.summary.data.controller.name%",
|
||||
"type": "readonly_text",
|
||||
"isEvaluated": true,
|
||||
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAME)"
|
||||
},
|
||||
{
|
||||
"label": "%arc.data.controller.summary.subscription%",
|
||||
"type": "readonly_text",
|
||||
"isEvaluated": true,
|
||||
"defaultValue": "$(AZDATA_NB_VAR_ARC_DISPLAY_SUBSCRIPTION)",
|
||||
"defaultValue": "$(AZDATA_NB_VAR_ARC_SUBSCRIPTION)",
|
||||
"inputWidth": "600"
|
||||
},
|
||||
{
|
||||
@@ -483,7 +571,30 @@
|
||||
"label": "%arc.data.controller.summary.location%",
|
||||
"type": "readonly_text",
|
||||
"isEvaluated": true,
|
||||
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_DISPLAY_LOCATION)"
|
||||
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_LOCATION)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "%arc.data.controller.summary.controller%",
|
||||
"fields": [
|
||||
{
|
||||
"label": "%arc.data.controller.summary.data.controller.namespace%",
|
||||
"type": "readonly_text",
|
||||
"isEvaluated": true,
|
||||
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAMESPACE)"
|
||||
},
|
||||
{
|
||||
"label": "%arc.data.controller.summary.data.controller.name%",
|
||||
"type": "readonly_text",
|
||||
"isEvaluated": true,
|
||||
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_NAME)"
|
||||
},
|
||||
{
|
||||
"label": "%arc.data.controller.connectivitymode%",
|
||||
"type": "readonly_text",
|
||||
"isEvaluated": true,
|
||||
"defaultValue": "$(AZDATA_NB_VAR_ARC_DATA_CONTROLLER_CONNECTIVITY_MODE)"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -510,7 +621,10 @@
|
||||
"description": "%resource.type.arc.sql.description%",
|
||||
"platforms": "*",
|
||||
"icon": "./images/miaa.svg",
|
||||
"tags": ["Hybrid", "SQL Server"],
|
||||
"tags": [
|
||||
"Hybrid",
|
||||
"SQL Server"
|
||||
],
|
||||
"providers": [
|
||||
{
|
||||
"notebookWizard": {
|
||||
@@ -559,18 +673,22 @@
|
||||
"type": "text",
|
||||
"defaultValue": "sqlinstance1",
|
||||
"required": true,
|
||||
"textValidationRequired": true,
|
||||
"textValidationRegex": "^[a-z]([-a-z0-9]{0,11}[a-z0-9])?$",
|
||||
"textValidationDescription": "%arc.sql.invalid.instance.name%"
|
||||
"validations" : [{
|
||||
"type": "regex_match",
|
||||
"regex": "^[a-z]([-a-z0-9]{0,11}[a-z0-9])?$",
|
||||
"description": "%arc.sql.invalid.instance.name%"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"label": "%arc.sql.username%",
|
||||
"variableName": "AZDATA_NB_VAR_SQL_USERNAME",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"textValidationRequired": true,
|
||||
"textValidationRegex": "^(?!sa$)",
|
||||
"textValidationDescription": "%arc.sql.invalid.username%"
|
||||
"validations" : [{
|
||||
"type": "regex_match",
|
||||
"regex": "^(?!sa$)",
|
||||
"description": "%arc.sql.invalid.username%"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"label": "%arc.password%",
|
||||
@@ -607,7 +725,14 @@
|
||||
"variableName": "AZDATA_NB_VAR_SQL_CORES_REQUEST",
|
||||
"type": "number",
|
||||
"min": 1,
|
||||
"required": false
|
||||
"required": false,
|
||||
"validations": [
|
||||
{
|
||||
"type": "<=",
|
||||
"target": "AZDATA_NB_VAR_SQL_CORES_LIMIT",
|
||||
"description": "%requested.cores.less.than.or.equal.to.cores.limit%"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "%arc.cores-limit.label%",
|
||||
@@ -615,7 +740,14 @@
|
||||
"variableName": "AZDATA_NB_VAR_SQL_CORES_LIMIT",
|
||||
"type": "number",
|
||||
"min": 1,
|
||||
"required": false
|
||||
"required": false,
|
||||
"validations": [
|
||||
{
|
||||
"type": ">=",
|
||||
"target": "AZDATA_NB_VAR_SQL_CORES_REQUEST",
|
||||
"description": "%cores.limit.greater.than.or.equal.to.requested.cores%"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "%arc.memory-request.label%",
|
||||
@@ -623,7 +755,12 @@
|
||||
"variableName": "AZDATA_NB_VAR_SQL_MEMORY_REQUEST",
|
||||
"type": "number",
|
||||
"min": 2,
|
||||
"required": false
|
||||
"required": false,
|
||||
"validations": [{
|
||||
"type": "<=",
|
||||
"target": "AZDATA_NB_VAR_SQL_MEMORY_LIMIT",
|
||||
"description": "%requested.memory.less.than.or.equal.to.memory.limit%"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"label": "%arc.memory-limit.label%",
|
||||
@@ -631,7 +768,12 @@
|
||||
"variableName": "AZDATA_NB_VAR_SQL_MEMORY_LIMIT",
|
||||
"type": "number",
|
||||
"min": 2,
|
||||
"required": false
|
||||
"required": false,
|
||||
"validations": [{
|
||||
"type": ">=",
|
||||
"target": "AZDATA_NB_VAR_SQL_MEMORY_REQUEST",
|
||||
"description": "%memory.limit.greater.than.or.equal.to.requested.memory%"
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -671,7 +813,10 @@
|
||||
"description": "%resource.type.arc.postgres.description%",
|
||||
"platforms": "*",
|
||||
"icon": "./images/postgres.svg",
|
||||
"tags": ["Hybrid", "PostgreSQL"],
|
||||
"tags": [
|
||||
"Hybrid",
|
||||
"PostgreSQL"
|
||||
],
|
||||
"providers": [
|
||||
{
|
||||
"notebookWizard": {
|
||||
@@ -719,9 +864,11 @@
|
||||
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_NAME",
|
||||
"type": "text",
|
||||
"description": "%arc.postgres.server.group.name.validation.description%",
|
||||
"textValidationRequired": true,
|
||||
"textValidationRegex": "^[a-z]([-a-z0-9]{0,10}[a-z0-9])?$",
|
||||
"textValidationDescription": "%arc.postgres.server.group.name.validation.description%",
|
||||
"validations" : [{
|
||||
"type": "regex_match",
|
||||
"regex": "^[a-z]([-a-z0-9]{0,10}[a-z0-9])?$",
|
||||
"description": "%arc.postgres.server.group.name.validation.description%"
|
||||
}],
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
@@ -738,6 +885,10 @@
|
||||
"description": "%arc.postgres.server.group.workers.description%",
|
||||
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_WORKERS",
|
||||
"type": "number",
|
||||
"validations": [{
|
||||
"type": "is_integer",
|
||||
"description": "%should.be.integer%"
|
||||
}],
|
||||
"defaultValue": "0",
|
||||
"min": 0
|
||||
},
|
||||
@@ -745,6 +896,10 @@
|
||||
"label": "%arc.postgres.server.group.port%",
|
||||
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_PORT",
|
||||
"type": "number",
|
||||
"validations": [{
|
||||
"type": "is_integer",
|
||||
"description": "%should.be.integer%"
|
||||
}],
|
||||
"defaultValue": "5432",
|
||||
"min": 1,
|
||||
"max": 65535
|
||||
@@ -825,28 +980,48 @@
|
||||
"description": "%arc.postgres.server.group.cores.request.description%",
|
||||
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_REQUEST",
|
||||
"type": "number",
|
||||
"min": 1
|
||||
"min": 1,
|
||||
"validations": [{
|
||||
"type": "<=",
|
||||
"target": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_LIMIT",
|
||||
"description": "%requested.cores.less.than.or.equal.to.cores.limit%"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"label": "%arc.postgres.server.group.cores.limit.label%",
|
||||
"description": "%arc.postgres.server.group.cores.limit.description%",
|
||||
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_LIMIT",
|
||||
"type": "number",
|
||||
"min": 1
|
||||
"min": 1,
|
||||
"validations": [{
|
||||
"type": ">=",
|
||||
"target": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_CORES_REQUEST",
|
||||
"description": "%cores.limit.greater.than.or.equal.to.requested.cores%"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"label": "%arc.postgres.server.group.memory.request.label%",
|
||||
"description": "%arc.postgres.server.group.memory.request.description%",
|
||||
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_REQUEST",
|
||||
"type": "number",
|
||||
"min": 0.25
|
||||
"min": 0.25,
|
||||
"validations": [{
|
||||
"type": "<=",
|
||||
"target": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_LIMIT",
|
||||
"description": "%requested.memory.less.than.or.equal.to.memory.limit%"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"label": "%arc.postgres.server.group.memory.limit.label%",
|
||||
"description": "%arc.postgres.server.group.memory.limit.description%",
|
||||
"variableName": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_LIMIT",
|
||||
"type": "number",
|
||||
"min": 0.25
|
||||
"min": 0.25,
|
||||
"validations": [{
|
||||
"type": ">=",
|
||||
"target": "AZDATA_NB_VAR_POSTGRES_SERVER_GROUP_MEMORY_REQUEST",
|
||||
"description": "%memory.limit.greater.than.or.equal.to.requested.memory%"
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -885,7 +1060,8 @@
|
||||
"dependencies": {
|
||||
"request": "^2.88.0",
|
||||
"uuid": "^8.3.0",
|
||||
"vscode-nls": "^4.1.2"
|
||||
"vscode-nls": "^4.1.2",
|
||||
"yamljs": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^5.2.5",
|
||||
@@ -893,6 +1069,7 @@
|
||||
"@types/request": "^2.48.3",
|
||||
"@types/sinon": "^9.0.4",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@types/yamljs": "^0.2.31",
|
||||
"mocha": "^5.2.0",
|
||||
"mocha-junit-reporter": "^1.17.0",
|
||||
"mocha-multi-reporters": "^1.1.7",
|
||||
|
||||
@@ -20,21 +20,35 @@
|
||||
"arc.data.controller.kube.cluster.context": "Cluster context",
|
||||
"arc.data.controller.cluster.config.profile.title": "Choose the config profile",
|
||||
"arc.data.controller.cluster.config.profile": "Config profile",
|
||||
"arc.data.controller.data.controller.create.title": "Provide details to create Azure Arc data controller",
|
||||
"arc.data.controller.project.details.title": "Project details",
|
||||
"arc.data.controller.create.azureconfig.title": "Azure and Connectivity Configuration",
|
||||
"arc.data.controller.connectivitymode.description": "Select the connectivity mode for the controller.",
|
||||
"arc.data.controller.create.controllerconfig.title": "Controller Configuration",
|
||||
"arc.data.controller.project.details.title": "Azure details",
|
||||
"arc.data.controller.project.details.description": "Select the subscription to manage deployed resources and costs. Use resource groups like folders to organize and manage all your resources.",
|
||||
"arc.data.controller.data.controller.details.title": "Data controller details",
|
||||
"arc.data.controller.data.controller.details.description": "Provide an Azure region and a name for your Azure Arc data controller. This name will be used to identify your Arc location for remote management and monitoring.",
|
||||
"arc.data.controller.arc.data.controller.namespace": "Data controller namespace",
|
||||
"arc.data.controller.arc.data.controller.namespace.validation.description": "Namespace must consist of lower case alphanumeric characters or '-', start/end with an alphanumeric character, and be 63 characters or fewer in length.",
|
||||
"arc.data.controller.arc.data.controller.name": "Data controller name",
|
||||
"arc.data.controller.arc.data.controller.name.validation.description": "Name must consist of lower case alphanumeric characters, '-' or '.', start/end with an alphanumeric character and be 253 characters or less in length.",
|
||||
"arc.data.controller.arc.data.controller.location": "Location",
|
||||
"arc.data.controller.details.title": "Data controller details",
|
||||
"arc.data.controller.details.description": "Provide a namespace, name and storage class for your Azure Arc data controller. This name will be used to identify your Arc instance for remote management and monitoring.",
|
||||
"arc.data.controller.namespace": "Data controller namespace",
|
||||
"arc.data.controller.namespace.validation.description": "Namespace must consist of lower case alphanumeric characters or '-', start/end with an alphanumeric character, and be 63 characters or fewer in length.",
|
||||
"arc.data.controller.name": "Data controller name",
|
||||
"arc.data.controller.name.validation.description": "Name must consist of lower case alphanumeric characters, '-' or '.', start/end with an alphanumeric character and be 253 characters or less in length.",
|
||||
"arc.data.controller.location": "Location",
|
||||
"arc.data.controller.admin.account.title": "Administrator account",
|
||||
"arc.data.controller.admin.account.name": "Data controller login",
|
||||
"arc.data.controller.admin.account.password": "Password",
|
||||
"arc.data.controller.admin.account.confirm.password": "Confirm password",
|
||||
"arc.data.controller.data.controller.create.summary.title": "Review your configuration",
|
||||
"arc.data.controller.connectivitymode": "Connectivity Mode",
|
||||
"arc.data.controller.direct": "Direct",
|
||||
"arc.data.controller.indirect": "Indirect",
|
||||
"arc.data.controller.serviceprincipal.description": "When deploying a controller in direct connected mode a Service Principal is required for uploading metrics to Azure. {0} about how to create this Service Principal and assign it the correct roles.",
|
||||
"arc.data.controller.spclientid": "Service Principal Client ID",
|
||||
"arc.data.controller.spclientid.description": "The Application (client) ID of the created Service Principal",
|
||||
"arc.data.controller.spclientid.validation.description": "The client ID must be a GUID in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"arc.data.controller.spclientsecret": "Service Principal Client Secret",
|
||||
"arc.data.controller.spclientsecret.description": "The password generated during creation of the Service Principal",
|
||||
"arc.data.controller.sptenantid": "Service Principal Tenant ID",
|
||||
"arc.data.controller.sptenantid.description": "The Tenant ID of the Service Principal. This must be the same as the Tenant ID of the subscription selected to create this controller for.",
|
||||
"arc.data.controller.sptenantid.validation.description": "The tenant ID must be a GUID in the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||
"arc.data.controller.create.summary.title": "Review your configuration",
|
||||
"arc.data.controller.summary.arc.data.controller": "Azure Arc data controller",
|
||||
"arc.data.controller.summary.estimated.cost.per.month": "Estimated cost per month",
|
||||
"arc.data.controller.summary.arc.by.microsoft" : "by Microsoft",
|
||||
@@ -55,8 +69,10 @@
|
||||
"arc.data.controller.summary.resource.group": "Resource group",
|
||||
"arc.data.controller.summary.data.controller.name": "Data controller name",
|
||||
"arc.data.controller.summary.data.controller.namespace": "Data controller namespace",
|
||||
"arc.data.controller.summary.controller": "Controller",
|
||||
"arc.data.controller.summary.location": "Location",
|
||||
"arc.data.controller.arc.data.controller.agreement": "I accept {0} and {1}.",
|
||||
"arc.data.controller.agreement": "I accept {0} and {1}.",
|
||||
"arc.data.controller.readmore": "Read more",
|
||||
"microsoft.agreement.privacy.statement":"Microsoft Privacy Statement",
|
||||
"deploy.script.action":"Script to notebook",
|
||||
"deploy.done.action":"Deploy",
|
||||
@@ -129,6 +145,11 @@
|
||||
"arc.postgres.server.group.memory.limit.label": "Memory limit (GB per node)",
|
||||
"arc.postgres.server.group.memory.limit.description": "The memory limit of the Postgres instance per node in GB.",
|
||||
"arc.agreement": "I accept {0} and {1}.",
|
||||
"arc.agreement.sql.terms.conditions":"Azure SQL managed instance - Azure Arc terms and conditions",
|
||||
"arc.agreement.postgres.terms.conditions":"Azure Arc enabled PostgreSQL Hyperscale terms and conditions"
|
||||
"arc.agreement.sql.terms.conditions": "Azure SQL managed instance - Azure Arc terms and conditions",
|
||||
"arc.agreement.postgres.terms.conditions": "Azure Arc enabled PostgreSQL Hyperscale terms and conditions",
|
||||
"should.be.integer": "Value must be an integer",
|
||||
"requested.cores.less.than.or.equal.to.cores.limit": "Requested cores must be less than or equal to cores limit",
|
||||
"cores.limit.greater.than.or.equal.to.requested.cores": "Cores limit must be greater than or equal to requested cores",
|
||||
"requested.memory.less.than.or.equal.to.memory.limit": "Requested memory must be less than or equal to memory limit",
|
||||
"memory.limit.greater.than.or.equal.to.requested.memory": "Memory limit must be greater than or equal to requested memory"
|
||||
}
|
||||
|
||||
@@ -4,11 +4,17 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as arc from 'arc';
|
||||
import * as rd from 'resource-deployment';
|
||||
import * as loc from '../localizedConstants';
|
||||
import { PasswordToControllerDialog } from '../ui/dialogs/connectControllerDialog';
|
||||
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
|
||||
import { ControllerTreeNode } from '../ui/tree/controllerTreeNode';
|
||||
import { UserCancelledError } from './utils';
|
||||
|
||||
export class UserCancelledError extends Error implements rd.ErrorWithType {
|
||||
public get type(): rd.ErrorType {
|
||||
return rd.ErrorType.userCancelled;
|
||||
}
|
||||
}
|
||||
export function arcApi(treeDataProvider: AzureArcTreeDataProvider): arc.IExtension {
|
||||
return {
|
||||
getRegisteredDataControllers: () => getRegisteredDataControllers(treeDataProvider),
|
||||
@@ -16,12 +22,13 @@ export function arcApi(treeDataProvider: AzureArcTreeDataProvider): arc.IExtensi
|
||||
reacquireControllerPassword: (controllerInfo: arc.ControllerInfo) => reacquireControllerPassword(treeDataProvider, controllerInfo)
|
||||
};
|
||||
}
|
||||
|
||||
export async function reacquireControllerPassword(treeDataProvider: AzureArcTreeDataProvider, controllerInfo: arc.ControllerInfo): Promise<string> {
|
||||
const dialog = new PasswordToControllerDialog(treeDataProvider);
|
||||
dialog.showDialog(controllerInfo);
|
||||
const model = await dialog.waitForClose();
|
||||
if (!model) {
|
||||
throw new UserCancelledError();
|
||||
throw new UserCancelledError(loc.userCancelledError);
|
||||
}
|
||||
return model.password;
|
||||
}
|
||||
|
||||
39
extensions/arc/src/common/kubeUtils.ts
Normal file
39
extensions/arc/src/common/kubeUtils.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as yamljs from 'yamljs';
|
||||
import * as loc from '../localizedConstants';
|
||||
import { throwUnless } from './utils';
|
||||
export interface KubeClusterContext {
|
||||
name: string;
|
||||
isCurrentContext: boolean;
|
||||
}
|
||||
|
||||
export function getKubeConfigClusterContexts(configFile: string): Promise<KubeClusterContext[]> {
|
||||
const config: any = yamljs.load(configFile);
|
||||
const rawContexts = <any[]>config['contexts'];
|
||||
throwUnless(rawContexts && rawContexts.length, loc.noContextFound(configFile));
|
||||
const currentContext = <string>config['current-context'];
|
||||
throwUnless(currentContext, loc.noCurrentContextFound(configFile));
|
||||
const contexts: KubeClusterContext[] = [];
|
||||
rawContexts.forEach(rawContext => {
|
||||
const name = <string>rawContext['name'];
|
||||
throwUnless(name, loc.noNameInContext(configFile));
|
||||
if (name) {
|
||||
contexts.push({
|
||||
name: name,
|
||||
isCurrentContext: name === currentContext
|
||||
});
|
||||
}
|
||||
});
|
||||
return Promise.resolve(contexts);
|
||||
}
|
||||
|
||||
export function getDefaultKubeConfigPath(): string {
|
||||
return path.join(os.homedir(), '.kube', 'config');
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ import * as vscode from 'vscode';
|
||||
import { ConnectionMode, IconPath, IconPathHelper } from '../constants';
|
||||
import * as loc from '../localizedConstants';
|
||||
|
||||
export class UserCancelledError extends Error { }
|
||||
|
||||
/**
|
||||
* Converts the resource type name into the localized Display Name for that type.
|
||||
* @param resourceType The resource type name to convert
|
||||
@@ -67,7 +65,7 @@ export function getResourceTypeIcon(resourceType: string | undefined): IconPath
|
||||
|
||||
/**
|
||||
* Returns the text to display for known connection modes
|
||||
* @param connectionMode The string repsenting the connection mode
|
||||
* @param connectionMode The string representing the connection mode
|
||||
*/
|
||||
export function getConnectionModeDisplayText(connectionMode: string | undefined): string {
|
||||
connectionMode = connectionMode ?? '';
|
||||
@@ -282,8 +280,18 @@ export function convertToGibibyteString(value: string): string {
|
||||
* @param condition
|
||||
* @param message
|
||||
*/
|
||||
export function throwUnless(condition: boolean, message?: string): asserts condition {
|
||||
export function throwUnless(condition: any, message?: string): asserts condition {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
export async function tryExecuteAction<T>(action: () => T | PromiseLike<T>): Promise<{ result: T | undefined, error: any }> {
|
||||
let error: any, result: T | undefined;
|
||||
try {
|
||||
result = await action();
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
return { result, error };
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<arc.IE
|
||||
|
||||
// register option sources
|
||||
const rdApi = <rd.IExtension>vscode.extensions.getExtension(rd.extension.name)?.exports;
|
||||
rdApi.registerOptionsSourceProvider(new ArcControllersOptionsSourceProvider(treeDataProvider));
|
||||
context.subscriptions.push(rdApi.registerOptionsSourceProvider(new ArcControllersOptionsSourceProvider(treeDataProvider)));
|
||||
|
||||
return arcApi(treeDataProvider);
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ export const feedback = localize('arc.feedback', "Feedback");
|
||||
export const selectConnectionString = localize('arc.selectConnectionString', "Select from available client connection strings below.");
|
||||
export const addingWokerNodes = localize('arc.addingWokerNodes', "adding worker nodes");
|
||||
export const workerNodesDescription = localize('arc.workerNodesDescription', "Expand your server group and scale your database by adding worker nodes.");
|
||||
export const configurationInformation = localize('arc.configurationInformation', "You can configure the number of CPU cores and storage size that will apply to both worker nodes and coordinator node. Each worker node will have the same configuration. Adjust the number of CPU cores and memory settings for your server group.");
|
||||
export const postgresConfigurationInformation = localize('arc.postgres.configurationInformation', "You can configure the number of CPU cores and storage size that will apply to both worker nodes and coordinator node. Each worker node will have the same configuration. Adjust the number of CPU cores and memory settings for your server group.");
|
||||
export const workerNodesInformation = localize('arc.workerNodeInformation', "In preview it is not possible to reduce the number of worker nodes. Please refer to documentation linked above for more information.");
|
||||
export const vCores = localize('arc.vCores', "vCores");
|
||||
export const ram = localize('arc.ram', "RAM");
|
||||
@@ -121,8 +121,9 @@ export const enterNewPassword = localize('arc.enterNewPassword', "Enter a new pa
|
||||
export const confirmNewPassword = localize('arc.confirmNewPassword', "Confirm the new password");
|
||||
export const learnAboutPostgresClients = localize('arc.learnAboutPostgresClients', "Learn more about Azure PostgreSQL Hyperscale client interfaces");
|
||||
export const scalingCompute = localize('arc.scalingCompute', "scaling compute vCores and memory.");
|
||||
export const computeAndStorageDescriptionPartOne = localize('arc.computeAndStorageDescriptionPartOne', "You can scale your Azure Arc enabled");
|
||||
export const computeAndStorageDescriptionPartTwo = localize('arc.computeAndStorageDescriptionPartTwo', "PostgreSQL Hyperscale server group by");
|
||||
export const postgresComputeAndStorageDescriptionPartOne = localize('arc.postgresComputeAndStorageDescriptionPartOne', "You can scale your Azure Arc enabled");
|
||||
export const miaaComputeAndStorageDescriptionPartOne = localize('arc.miaaComputeAndStorageDescriptionPartOne', "You can scale your Azure SQL managed instance - Azure Arc by");
|
||||
export const postgresComputeAndStorageDescriptionPartTwo = localize('arc.postgres.computeAndStorageDescriptionPartTwo', "PostgreSQL Hyperscale server group by");
|
||||
export const computeAndStorageDescriptionPartThree = localize('arc.computeAndStorageDescriptionPartThree', "without downtime and by");
|
||||
export const computeAndStorageDescriptionPartFour = localize('arc.computeAndStorageDescriptionPartFour', "Before doing so, you need to ensure");
|
||||
export const computeAndStorageDescriptionPartFive = localize('arc.computeAndStorageDescriptionPartFive', "there are sufficient resources available");
|
||||
@@ -138,7 +139,6 @@ export const coresRequest = localize('arc.coresRequest', "CPU request:");
|
||||
export const memoryLimit = localize('arc.memoryLimit', "Memory limit (in GB):");
|
||||
export const memoryRequest = localize('arc.memoryRequest', "Memory request (in GB):");
|
||||
export const workerValidationErrorMessage = localize('arc.workerValidationErrorMessage', "The number of workers cannot be decreased.");
|
||||
export const coresValidationErrorMessage = localize('arc.coresValidationErrorMessage', "Valid CPU resource quantities are strictly positive.");
|
||||
export const memoryRequestValidationErrorMessage = localize('arc.memoryRequestValidationErrorMessage', "Memory request must be at least 0.25Gib");
|
||||
export const memoryLimitValidationErrorMessage = localize('arc.memoryLimitValidationErrorMessage', "Memory limit must be at least 0.25Gib");
|
||||
export const arcResources = localize('arc.arcResources', "Azure Arc Resources");
|
||||
@@ -171,6 +171,7 @@ export function numVCores(vCores: string | undefined): string {
|
||||
}
|
||||
}
|
||||
export function updated(when: string): string { return localize('arc.updated', "Updated {0}", when); }
|
||||
export function validationMin(min: number): string { return localize('arc.validationMin', "Value must be greater than or equal to {0}.", min); }
|
||||
|
||||
// Errors
|
||||
export const connectionRequired = localize('arc.connectionRequired', "A connection is required to show all properties. Click refresh to re-enter connection information");
|
||||
@@ -201,3 +202,7 @@ export const variableValueFetchForUnsupportedVariable = (variableName: string) =
|
||||
export const isPasswordFetchForUnsupportedVariable = (variableName: string) => localize('getIsPassword.unknownVariableName', "Attempt to get isPassword for unknown variable:{0}", variableName);
|
||||
export const noControllerInfoFound = (name: string) => localize('noControllerInfoFound', "Controller Info could not be found with name: {0}", name);
|
||||
export const noPasswordFound = (controllerName: string) => localize('noPasswordFound', "Password could not be retrieved for controller: {0} and user did not provide a password. Please retry later.", controllerName);
|
||||
export const noContextFound = (configFile: string) => localize('noContextFound', "No 'contexts' found in the config file: {0}", configFile);
|
||||
export const noCurrentContextFound = (configFile: string) => localize('noCurrentContextFound', "No context is marked as 'current-context' in the config file: {0}", configFile);
|
||||
export const noNameInContext = (configFile: string) => localize('noNameInContext', "No name field was found in a cluster context in the config file: {0}", configFile);
|
||||
export const userCancelledError = localize('userCancelledError', "User cancelled the dialog");
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { ControllerInfo, ResourceType } from 'arc';
|
||||
import * as azdataExt from 'azdata-ext';
|
||||
import * as vscode from 'vscode';
|
||||
import { UserCancelledError } from '../common/utils';
|
||||
import { UserCancelledError } from '../common/api';
|
||||
import * as loc from '../localizedConstants';
|
||||
import { ConnectToControllerDialog } from '../ui/dialogs/connectControllerDialog';
|
||||
import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
|
||||
@@ -71,7 +71,7 @@ export class ControllerModel {
|
||||
await this.treeDataProvider.addOrUpdateController(model.controllerModel, model.password, false);
|
||||
this._password = model.password;
|
||||
} else {
|
||||
throw new UserCancelledError();
|
||||
throw new UserCancelledError(loc.userCancelledError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@ import { MiaaResourceInfo } from 'arc';
|
||||
import * as azdata from 'azdata';
|
||||
import * as azdataExt from 'azdata-ext';
|
||||
import * as vscode from 'vscode';
|
||||
import { UserCancelledError } from '../common/api';
|
||||
import { Deferred } from '../common/promise';
|
||||
import { createCredentialId, parseIpAndPort, UserCancelledError } from '../common/utils';
|
||||
import { createCredentialId, parseIpAndPort } from '../common/utils';
|
||||
import { credentialNamespace } from '../constants';
|
||||
import * as loc from '../localizedConstants';
|
||||
import { ConnectToSqlDialog } from '../ui/dialogs/connectSqlDialog';
|
||||
|
||||
@@ -17,7 +17,7 @@ import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider';
|
||||
*/
|
||||
export class ArcControllersOptionsSourceProvider implements rd.IOptionsSourceProvider {
|
||||
private _cacheManager = new CacheManager<string, string>();
|
||||
readonly optionsSourceId = 'arc.controllers';
|
||||
readonly id = 'arc.controllers';
|
||||
constructor(private _treeProvider: AzureArcTreeDataProvider) { }
|
||||
|
||||
async getOptions(): Promise<string[] | azdata.CategoryValue[]> {
|
||||
|
||||
62
extensions/arc/src/test/common/kubeUtils.test.ts
Normal file
62
extensions/arc/src/test/common/kubeUtils.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'mocha';
|
||||
import * as path from 'path';
|
||||
import * as sinon from 'sinon';
|
||||
import * as yamljs from 'yamljs';
|
||||
import { getDefaultKubeConfigPath, getKubeConfigClusterContexts, KubeClusterContext } from '../../common/kubeUtils';
|
||||
import { tryExecuteAction } from '../../common/utils';
|
||||
|
||||
const kubeConfig =
|
||||
{
|
||||
'contexts': [
|
||||
{
|
||||
'context': {
|
||||
'cluster': 'docker-desktop',
|
||||
'user': 'docker-desktop'
|
||||
},
|
||||
'name': 'docker-for-desktop'
|
||||
},
|
||||
{
|
||||
'context': {
|
||||
'cluster': 'kubernetes',
|
||||
'user': 'kubernetes-admin'
|
||||
},
|
||||
'name': 'kubernetes-admin@kubernetes'
|
||||
}
|
||||
],
|
||||
'current-context': 'docker-for-desktop'
|
||||
};
|
||||
describe('KubeUtils', function (): void {
|
||||
const configFile = 'kubeConfig';
|
||||
|
||||
afterEach('KubeUtils cleanup', () => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('getDefaultKubeConfigPath', async () => {
|
||||
getDefaultKubeConfigPath().should.endWith(path.join('.kube', 'config'));
|
||||
});
|
||||
|
||||
describe('get Kube Config Cluster Contexts', () => {
|
||||
it('success', async () => {
|
||||
sinon.stub(yamljs, 'load').returns(<any>kubeConfig);
|
||||
const verifyContexts = (contexts: KubeClusterContext[], testName: string) => {
|
||||
contexts.length.should.equal(2, `test: ${testName} failed`);
|
||||
contexts[0].name.should.equal('docker-for-desktop', `test: ${testName} failed`);
|
||||
contexts[0].isCurrentContext.should.be.true(`test: ${testName} failed`);
|
||||
contexts[1].name.should.equal('kubernetes-admin@kubernetes', `test: ${testName} failed`);
|
||||
contexts[1].isCurrentContext.should.be.false(`test: ${testName} failed`);
|
||||
};
|
||||
verifyContexts(await getKubeConfigClusterContexts(configFile), 'getKubeConfigClusterContexts');
|
||||
});
|
||||
it('throws error when unable to load config file', async () => {
|
||||
const error = new Error('unknown error accessing file');
|
||||
sinon.stub(yamljs, 'load').throws(error); //erroring config file load
|
||||
((await tryExecuteAction(() => getKubeConfigClusterContexts(configFile))).error).should.equal(error, `test: getKubeConfigClusterContexts failed`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -49,6 +49,7 @@ export class FakeAzdataApi implements azdataExt.IAzdataApi {
|
||||
replaceEngineSettings?: boolean,
|
||||
workers?: number
|
||||
},
|
||||
_engineVersion?: string,
|
||||
_additionalEnvVars?: { [key: string]: string }): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }
|
||||
}
|
||||
},
|
||||
@@ -56,7 +57,16 @@ export class FakeAzdataApi implements azdataExt.IAzdataApi {
|
||||
mi: {
|
||||
delete(_name: string): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); },
|
||||
async list(): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiListResult[]>> { return <any>{ result: self.miaaInstances }; },
|
||||
show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> { throw new Error('Method not implemented.'); }
|
||||
show(_name: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> { throw new Error('Method not implemented.'); },
|
||||
edit(
|
||||
_name: string,
|
||||
_args: {
|
||||
coresLimit?: string,
|
||||
coresRequest?: string,
|
||||
memoryLimit?: string,
|
||||
memoryRequest?: string,
|
||||
noWait?: boolean
|
||||
}): Promise<azdataExt.AzdataOutput<void>> { throw new Error('Method not implemented.'); }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
91
extensions/arc/src/test/mocks/fakeRadioButton.ts
Normal file
91
extensions/arc/src/test/mocks/fakeRadioButton.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export class FakeRadioButton implements azdata.RadioButtonComponent {
|
||||
|
||||
private _onDidClickEmitter = new vscode.EventEmitter<any>();
|
||||
|
||||
onDidClick = this._onDidClickEmitter.event;
|
||||
|
||||
constructor(props: azdata.RadioButtonProperties) {
|
||||
this.label = props.label;
|
||||
this.value = props.value;
|
||||
this.checked = props.checked;
|
||||
this.enabled = props.enabled;
|
||||
}
|
||||
|
||||
//#region RadioButtonProperties implementation
|
||||
label?: string;
|
||||
value?: string;
|
||||
checked?: boolean;
|
||||
//#endregion
|
||||
|
||||
click() {
|
||||
this.checked = true;
|
||||
this._onDidClickEmitter.fire(this);
|
||||
}
|
||||
//#region Component Implementation
|
||||
id: string = '';
|
||||
updateProperties(_properties: { [key: string]: any; }): Thenable<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
updateProperty(_key: string, _value: any): Thenable<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
updateCssStyles(_cssStyles: { [key: string]: string; }): Thenable<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
onValidityChanged: vscode.Event<boolean> = <vscode.Event<boolean>>{};
|
||||
valid: boolean = false;
|
||||
validate(): Thenable<boolean> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
focus(): Thenable<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
ariaHidden?: boolean | undefined;
|
||||
//#endregion
|
||||
|
||||
//#region ComponentProperties Implementation
|
||||
height?: number | string;
|
||||
width?: number | string;
|
||||
/**
|
||||
* The position CSS property. Empty by default.
|
||||
* This is particularly useful if laying out components inside a FlexContainer and
|
||||
* the size of the component is meant to be a fixed size. In this case the position must be
|
||||
* set to 'absolute', with the parent FlexContainer having 'relative' position.
|
||||
* Without this the component will fail to correctly size itself
|
||||
*/
|
||||
position?: azdata.PositionType;
|
||||
/**
|
||||
* Whether the component is enabled in the DOM
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* Corresponds to the display CSS property for the element
|
||||
*/
|
||||
display?: azdata.DisplayType;
|
||||
/**
|
||||
* Corresponds to the aria-label accessibility attribute for this component
|
||||
*/
|
||||
ariaLabel?: string;
|
||||
/**
|
||||
* Corresponds to the role accessibility attribute for this component
|
||||
*/
|
||||
ariaRole?: string;
|
||||
/**
|
||||
* Corresponds to the aria-selected accessibility attribute for this component
|
||||
*/
|
||||
ariaSelected?: boolean;
|
||||
/**
|
||||
* Matches the CSS style key and its available values.
|
||||
*/
|
||||
CSSStyles?: { [key: string]: string };
|
||||
//#endregion
|
||||
|
||||
}
|
||||
@@ -11,7 +11,8 @@ import * as sinon from 'sinon';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import * as vscode from 'vscode';
|
||||
import { UserCancelledError } from '../../common/utils';
|
||||
import * as loc from '../../localizedConstants';
|
||||
import { UserCancelledError } from '../../common/api';
|
||||
import { ControllerModel } from '../../models/controllerModel';
|
||||
import { ConnectToControllerDialog } from '../../ui/dialogs/connectControllerDialog';
|
||||
import { AzureArcTreeDataProvider } from '../../ui/tree/azureArcTreeDataProvider';
|
||||
@@ -39,11 +40,11 @@ describe('ControllerModel', function (): void {
|
||||
// Returning an undefined model here indicates that the dialog closed without clicking "Ok" - usually through the user clicking "Cancel"
|
||||
sinon.stub(ConnectToControllerDialog.prototype, 'waitForClose').returns(Promise.resolve(undefined));
|
||||
const model = new ControllerModel(new AzureArcTreeDataProvider(mockExtensionContext.object), { id: uuid(), url: '127.0.0.1', username: 'admin', name: 'arc', rememberPassword: true, resources: [] });
|
||||
await should(model.azdataLogin()).be.rejectedWith(new UserCancelledError());
|
||||
await should(model.azdataLogin()).be.rejectedWith(new UserCancelledError(loc.userCancelledError));
|
||||
});
|
||||
|
||||
it('Reads password from cred store', async function (): Promise<void> {
|
||||
const password = 'password123';
|
||||
const password = 'password123'; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Test password, not actually used")]
|
||||
|
||||
// Set up cred store to return our password
|
||||
const credProviderMock = TypeMoq.Mock.ofType<azdata.CredentialProvider>();
|
||||
@@ -64,7 +65,7 @@ describe('ControllerModel', function (): void {
|
||||
});
|
||||
|
||||
it('Prompt for password when not in cred store', async function (): Promise<void> {
|
||||
const password = 'password123';
|
||||
const password = 'password123'; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Stub value for testing")]
|
||||
|
||||
// Set up cred store to return empty password
|
||||
const credProviderMock = TypeMoq.Mock.ofType<azdata.CredentialProvider>();
|
||||
@@ -90,7 +91,7 @@ describe('ControllerModel', function (): void {
|
||||
});
|
||||
|
||||
it('Prompt for password when rememberPassword is true but prompt reconnect is true', async function (): Promise<void> {
|
||||
const password = 'password123';
|
||||
const password = 'password123'; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Stub value for testing")]
|
||||
// Set up cred store to return a password to start with
|
||||
const credProviderMock = TypeMoq.Mock.ofType<azdata.CredentialProvider>();
|
||||
credProviderMock.setup(x => x.readCredential(TypeMoq.It.isAny())).returns(() => Promise.resolve({ credentialId: 'id', password: 'originalPassword' }));
|
||||
@@ -116,7 +117,7 @@ describe('ControllerModel', function (): void {
|
||||
});
|
||||
|
||||
it('Prompt for password when we already have a password but prompt reconnect is true', async function (): Promise<void> {
|
||||
const password = 'password123';
|
||||
const password = 'password123'; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Stub value for testing")]
|
||||
// Set up cred store to return a password to start with
|
||||
const credProviderMock = TypeMoq.Mock.ofType<azdata.CredentialProvider>();
|
||||
credProviderMock.setup(x => x.readCredential(TypeMoq.It.isAny())).returns(() => Promise.resolve({ credentialId: 'id', password: 'originalPassword' }));
|
||||
|
||||
@@ -3,8 +3,66 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as TypeMoq from 'typemoq';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export function createModelViewMock() {
|
||||
const mockModelBuilder = TypeMoq.Mock.ofType<azdata.ModelBuilder>();
|
||||
const mockTextBuilder = setupMockComponentBuilder<azdata.TextComponent, azdata.TextComponentProperties>();
|
||||
const mockInputBoxBuilder = setupMockComponentBuilder<azdata.InputBoxComponent, azdata.InputBoxProperties>();
|
||||
const mockRadioButtonBuilder = setupMockComponentBuilder<azdata.RadioButtonComponent, azdata.RadioButtonProperties>();
|
||||
const mockDivBuilder = setupMockContainerBuilder<azdata.DivContainer, azdata.DivContainerProperties, azdata.DivBuilder>();
|
||||
const mockLoadingBuilder = setupMockLoadingBuilder();
|
||||
mockModelBuilder.setup(b => b.loadingComponent()).returns(() => mockLoadingBuilder.object);
|
||||
mockModelBuilder.setup(b => b.text()).returns(() => mockTextBuilder.object);
|
||||
mockModelBuilder.setup(b => b.inputBox()).returns(() => mockInputBoxBuilder.object);
|
||||
mockModelBuilder.setup(b => b.radioButton()).returns(() => mockRadioButtonBuilder.object);
|
||||
mockModelBuilder.setup(b => b.divContainer()).returns(() => mockDivBuilder.object);
|
||||
const mockModelView = TypeMoq.Mock.ofType<azdata.ModelView>();
|
||||
mockModelView.setup(mv => mv.modelBuilder).returns(() => mockModelBuilder.object);
|
||||
return { mockModelView, mockModelBuilder, mockTextBuilder, mockInputBoxBuilder, mockRadioButtonBuilder, mockDivBuilder };
|
||||
}
|
||||
|
||||
function setupMockLoadingBuilder(
|
||||
loadingBuilderGetter?: (item: azdata.Component) => azdata.LoadingComponentBuilder,
|
||||
mockLoadingBuilder?: TypeMoq.IMock<azdata.LoadingComponentBuilder>
|
||||
): TypeMoq.IMock<azdata.LoadingComponentBuilder> {
|
||||
mockLoadingBuilder = mockLoadingBuilder ?? setupMockComponentBuilder<azdata.LoadingComponent, azdata.LoadingComponentProperties, azdata.LoadingComponentBuilder>();
|
||||
let item: azdata.Component;
|
||||
mockLoadingBuilder.setup(b => b.withItem(TypeMoq.It.isAny())).callback((_item) => item = _item).returns(() => loadingBuilderGetter ? loadingBuilderGetter(item) : mockLoadingBuilder!.object);
|
||||
return mockLoadingBuilder;
|
||||
}
|
||||
|
||||
export function setupMockComponentBuilder<T extends azdata.Component, P extends azdata.ComponentProperties, B extends azdata.ComponentBuilder<T, P> = azdata.ComponentBuilder<T, P>>(
|
||||
componentGetter?: (props: P) => T,
|
||||
mockComponentBuilder?: TypeMoq.IMock<B>,
|
||||
): TypeMoq.IMock<B> {
|
||||
mockComponentBuilder = mockComponentBuilder ?? TypeMoq.Mock.ofType<B>();
|
||||
const returnComponent = TypeMoq.Mock.ofType<T>();
|
||||
// Need to setup 'then' for when a mocked object is resolved otherwise the test will hang : https://github.com/florinn/typemoq/issues/66
|
||||
returnComponent.setup((x: any) => x.then).returns(() => { });
|
||||
let compProps: P;
|
||||
mockComponentBuilder.setup(b => b.withProperties(TypeMoq.It.isAny())).callback((props: P) => compProps = props).returns(() => mockComponentBuilder!.object);
|
||||
mockComponentBuilder.setup(b => b.component()).returns(() => {
|
||||
return componentGetter ? componentGetter(compProps) : Object.assign<T, P>(Object.assign({}, returnComponent.object), compProps);
|
||||
});
|
||||
|
||||
// For now just have these be passthrough - can hook up additional functionality later if needed
|
||||
mockComponentBuilder.setup(b => b.withValidation(TypeMoq.It.isAny())).returns(() => mockComponentBuilder!.object);
|
||||
return mockComponentBuilder;
|
||||
}
|
||||
|
||||
export function setupMockContainerBuilder<T extends azdata.Container<any, any>, P extends azdata.ComponentProperties, B extends azdata.ContainerBuilder<T, any, any, any> = azdata.ContainerBuilder<T, any, any, any>>(
|
||||
mockContainerBuilder?: TypeMoq.IMock<B>
|
||||
): TypeMoq.IMock<B> {
|
||||
mockContainerBuilder = mockContainerBuilder ?? setupMockComponentBuilder<T, P, B>();
|
||||
// For now just have these be passthrough - can hook up additional functionality later if needed
|
||||
mockContainerBuilder.setup(b => b.withItems(TypeMoq.It.isAny(), undefined)).returns(() => mockContainerBuilder!.object);
|
||||
mockContainerBuilder.setup(b => b.withLayout(TypeMoq.It.isAny())).returns(() => mockContainerBuilder!.object);
|
||||
return mockContainerBuilder;
|
||||
}
|
||||
|
||||
export class MockInputBox implements vscode.InputBox {
|
||||
private _value: string = '';
|
||||
public get value(): string {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import * as should from 'should';
|
||||
import { getErrorMessage } from '../../../common/utils';
|
||||
import { RadioOptionsGroup, RadioOptionsInfo } from '../../../ui/components/radioOptionsGroup';
|
||||
import { FakeRadioButton } from '../../mocks/fakeRadioButton';
|
||||
import { setupMockComponentBuilder, createModelViewMock } from '../../stubs';
|
||||
|
||||
|
||||
const loadingError = new Error('Error loading options');
|
||||
const radioOptionsInfo = <RadioOptionsInfo>{
|
||||
values: [
|
||||
'value1',
|
||||
'value2'
|
||||
],
|
||||
defaultValue: 'value2'
|
||||
};
|
||||
const divItems: azdata.Component[] = [];
|
||||
let radioOptionsGroup: RadioOptionsGroup;
|
||||
|
||||
|
||||
describe('radioOptionsGroup', function (): void {
|
||||
beforeEach(async () => {
|
||||
const { mockModelView, mockRadioButtonBuilder, mockDivBuilder } = createModelViewMock();
|
||||
mockRadioButtonBuilder.reset(); // reset any previous mock so that we can set our own.
|
||||
setupMockComponentBuilder<azdata.RadioButtonComponent, azdata.RadioButtonProperties>(
|
||||
(props) => new FakeRadioButton(props),
|
||||
mockRadioButtonBuilder,
|
||||
);
|
||||
mockDivBuilder.reset(); // reset previous setups so new setups we are about to create will replace the setups instead creating a recording chain
|
||||
// create new setups for the DivContainer with custom behavior
|
||||
setupMockComponentBuilder<azdata.DivContainer, azdata.DivContainerProperties, azdata.DivBuilder>(
|
||||
() => <azdata.DivContainer>{
|
||||
addItem: (item) => { divItems.push(item); },
|
||||
clearItems: () => { divItems.length = 0; },
|
||||
get items() { return divItems; },
|
||||
},
|
||||
mockDivBuilder
|
||||
);
|
||||
radioOptionsGroup = new RadioOptionsGroup(mockModelView.object, (_disposable) => { });
|
||||
await radioOptionsGroup.load(async () => radioOptionsInfo);
|
||||
});
|
||||
|
||||
it('verify construction and load', async () => {
|
||||
should(radioOptionsGroup).not.be.undefined();
|
||||
should(radioOptionsGroup.value).not.be.undefined();
|
||||
radioOptionsGroup.value!.should.equal('value2', 'radio options group should be the default checked value');
|
||||
// verify all the radioButtons created in the group
|
||||
verifyRadioGroup();
|
||||
});
|
||||
|
||||
it('onClick', async () => {
|
||||
// click the radioButton corresponding to 'value1'
|
||||
(divItems as FakeRadioButton[]).filter(r => r.value === 'value1').pop()!.click();
|
||||
radioOptionsGroup.value!.should.equal('value1', 'radio options group should correspond to the radioButton that we clicked');
|
||||
// verify all the radioButtons created in the group
|
||||
verifyRadioGroup();
|
||||
});
|
||||
|
||||
it('load throws', async () => {
|
||||
radioOptionsGroup.load(() => { throw loadingError; });
|
||||
//in error case radioButtons array wont hold radioButtons but holds a TextComponent with value equal to error string
|
||||
divItems.length.should.equal(1, 'There is should be only one element in the divContainer when loading error happens');
|
||||
const label = divItems[0] as azdata.TextComponent;
|
||||
should(label.value).not.be.undefined();
|
||||
label.value!.should.deepEqual(getErrorMessage(loadingError));
|
||||
should(label.CSSStyles).not.be.undefined();
|
||||
should(label.CSSStyles!.color).not.be.undefined();
|
||||
label.CSSStyles!.color.should.equal('Red');
|
||||
});
|
||||
});
|
||||
|
||||
function verifyRadioGroup() {
|
||||
const radioButtons = divItems as FakeRadioButton[];
|
||||
radioButtons.length.should.equal(radioOptionsInfo.values!.length);
|
||||
radioButtons.forEach(rb => {
|
||||
should(rb.label).not.be.undefined();
|
||||
should(rb.value).not.be.undefined();
|
||||
should(rb.enabled).not.be.undefined();
|
||||
rb.label!.should.equal(rb.value);
|
||||
rb.enabled!.should.be.true();
|
||||
});
|
||||
}
|
||||
|
||||
72
extensions/arc/src/ui/components/radioOptionsGroup.ts
Normal file
72
extensions/arc/src/ui/components/radioOptionsGroup.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { getErrorMessage } from '../../common/utils';
|
||||
|
||||
export interface RadioOptionsInfo {
|
||||
values?: string[],
|
||||
defaultValue: string
|
||||
}
|
||||
|
||||
export class RadioOptionsGroup {
|
||||
static id: number = 1;
|
||||
private _divContainer!: azdata.DivContainer;
|
||||
private _loadingBuilder: azdata.LoadingComponentBuilder;
|
||||
private _currentRadioOption!: azdata.RadioButtonComponent;
|
||||
|
||||
constructor(private _view: azdata.ModelView, private _onNewDisposableCreated: (disposable: vscode.Disposable) => void, private _groupName: string = `RadioOptionsGroup${RadioOptionsGroup.id++}`) {
|
||||
const divBuilder = this._view.modelBuilder.divContainer();
|
||||
const divBuilderWithProperties = divBuilder.withProperties<azdata.DivContainerProperties>({ clickable: false });
|
||||
this._divContainer = divBuilderWithProperties.component();
|
||||
const loadingComponentBuilder = this._view.modelBuilder.loadingComponent();
|
||||
this._loadingBuilder = loadingComponentBuilder.withItem(this._divContainer);
|
||||
}
|
||||
|
||||
public component(): azdata.LoadingComponent {
|
||||
return this._loadingBuilder.component();
|
||||
}
|
||||
|
||||
async load(optionsInfoGetter: () => Promise<RadioOptionsInfo>): Promise<void> {
|
||||
this.component().loading = true;
|
||||
this._divContainer.clearItems();
|
||||
try {
|
||||
const optionsInfo = await optionsInfoGetter();
|
||||
const options = optionsInfo.values!;
|
||||
let defaultValue: string = optionsInfo.defaultValue!;
|
||||
options.forEach((option: string) => {
|
||||
const radioOption = this._view!.modelBuilder.radioButton().withProperties<azdata.RadioButtonProperties>({
|
||||
label: option,
|
||||
checked: option === defaultValue,
|
||||
name: this._groupName,
|
||||
value: option,
|
||||
enabled: true
|
||||
}).component();
|
||||
if (radioOption.checked) {
|
||||
this._currentRadioOption = radioOption;
|
||||
}
|
||||
this._onNewDisposableCreated(radioOption.onDidClick(() => {
|
||||
if (this._currentRadioOption !== radioOption) {
|
||||
// uncheck the previously saved radio option, the ui gets handled correctly even if we did not do this due to the use of the 'groupName',
|
||||
// however, the checked properties on the radio button do not get updated, so while the stuff works even if we left the previous option checked,
|
||||
// it is just better to keep things clean.
|
||||
this._currentRadioOption.checked = false;
|
||||
this._currentRadioOption = radioOption;
|
||||
}
|
||||
}));
|
||||
this._divContainer.addItem(radioOption);
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
const errorLabel = this._view!.modelBuilder.text().withProperties({ value: getErrorMessage(e), CSSStyles: { 'color': 'Red' } }).component();
|
||||
this._divContainer.addItem(errorLabel);
|
||||
}
|
||||
this.component().loading = false;
|
||||
}
|
||||
|
||||
get value(): string | undefined {
|
||||
return this._currentRadioOption?.value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as azdata from 'azdata';
|
||||
import * as azdataExt from 'azdata-ext';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { IconPathHelper, cssStyles } from '../../../constants';
|
||||
import { DashboardPage } from '../../components/dashboardPage';
|
||||
import { convertToGibibyteString } from '../../../common/utils';
|
||||
import { MiaaModel } from '../../../models/miaaModel';
|
||||
|
||||
export class MiaaComputeAndStoragePage extends DashboardPage {
|
||||
|
||||
private configurationContainer?: azdata.DivContainer;
|
||||
private coresLimitBox?: azdata.InputBoxComponent;
|
||||
private coresRequestBox?: azdata.InputBoxComponent;
|
||||
private memoryLimitBox?: azdata.InputBoxComponent;
|
||||
private memoryRequestBox?: azdata.InputBoxComponent;
|
||||
|
||||
private discardButton?: azdata.ButtonComponent;
|
||||
private saveButton?: azdata.ButtonComponent;
|
||||
|
||||
private saveArgs: {
|
||||
coresLimit?: string,
|
||||
coresRequest?: string,
|
||||
memoryLimit?: string,
|
||||
memoryRequest?: string
|
||||
} = {};
|
||||
|
||||
private readonly _azdataApi: azdataExt.IExtension;
|
||||
|
||||
constructor(protected modelView: azdata.ModelView, private _miaaModel: MiaaModel) {
|
||||
super(modelView);
|
||||
this._azdataApi = vscode.extensions.getExtension(azdataExt.extension.name)?.exports;
|
||||
|
||||
this.initializeConfigurationBoxes();
|
||||
|
||||
this.disposables.push(this._miaaModel.onConfigUpdated(
|
||||
() => this.eventuallyRunOnInitialized(() => this.handleServiceUpdated())));
|
||||
}
|
||||
|
||||
protected get title(): string {
|
||||
return loc.computeAndStorage;
|
||||
}
|
||||
|
||||
protected get id(): string {
|
||||
return 'miaa-compute-and-storage';
|
||||
}
|
||||
|
||||
protected get icon(): { dark: string; light: string; } {
|
||||
return IconPathHelper.computeStorage;
|
||||
}
|
||||
|
||||
protected get container(): azdata.Component {
|
||||
const root = this.modelView.modelBuilder.divContainer().component();
|
||||
const content = this.modelView.modelBuilder.divContainer().component();
|
||||
root.addItem(content, { CSSStyles: { 'margin': '20px' } });
|
||||
|
||||
content.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: loc.computeAndStorage,
|
||||
CSSStyles: { ...cssStyles.title }
|
||||
}).component());
|
||||
|
||||
const infoComputeStorage_p1 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: loc.miaaComputeAndStorageDescriptionPartOne,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px', 'max-width': 'auto' }
|
||||
}).component();
|
||||
|
||||
const memoryVCoreslink = this.modelView.modelBuilder.hyperlink().withProperties<azdata.HyperlinkComponentProperties>({
|
||||
label: loc.scalingCompute,
|
||||
url: 'https://docs.microsoft.com/azure/azure-arc/data/configure-managed-instance',
|
||||
CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component();
|
||||
|
||||
const infoComputeStorage_p4 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: loc.computeAndStorageDescriptionPartFour,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component();
|
||||
|
||||
const infoComputeStorage_p5 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: loc.computeAndStorageDescriptionPartFive,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component();
|
||||
|
||||
const infoComputeStorage_p6 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: loc.computeAndStorageDescriptionPartSix,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component();
|
||||
|
||||
const computeInfoAndLinks = this.modelView.modelBuilder.flexContainer()
|
||||
.withLayout({ flexWrap: 'wrap' })
|
||||
.withItems([
|
||||
infoComputeStorage_p1,
|
||||
memoryVCoreslink,
|
||||
infoComputeStorage_p4,
|
||||
infoComputeStorage_p5,
|
||||
infoComputeStorage_p6
|
||||
], { CSSStyles: { 'margin-right': '5px' } }).component();
|
||||
content.addItem(computeInfoAndLinks, { CSSStyles: { 'min-height': '30px' } });
|
||||
|
||||
this.configurationContainer = this.modelView.modelBuilder.divContainer().component();
|
||||
this.configurationContainer.addItems(this.createUserInputSection(), { CSSStyles: { 'min-height': '30px' } });
|
||||
content.addItem(this.configurationContainer, { CSSStyles: { 'margin-top': '30px' } });
|
||||
|
||||
this.initialized = true;
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
protected get toolbarContainer(): azdata.ToolbarContainer {
|
||||
// Save Edits
|
||||
this.saveButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
||||
label: loc.saveText,
|
||||
iconPath: IconPathHelper.save,
|
||||
enabled: false
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.saveButton.onDidClick(async () => {
|
||||
this.saveButton!.enabled = false;
|
||||
try {
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: loc.updatingInstance(this._miaaModel.info.name),
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
try {
|
||||
await this._azdataApi.azdata.arc.sql.mi.edit(
|
||||
this._miaaModel.info.name, this.saveArgs);
|
||||
} catch (err) {
|
||||
this.saveButton!.enabled = true;
|
||||
throw err;
|
||||
}
|
||||
|
||||
await this._miaaModel.refresh();
|
||||
}
|
||||
);
|
||||
|
||||
vscode.window.showInformationMessage(loc.instanceUpdated(this._miaaModel.info.name));
|
||||
|
||||
this.discardButton!.enabled = false;
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.instanceUpdateFailed(this._miaaModel.info.name, error));
|
||||
}
|
||||
}));
|
||||
|
||||
// Discard
|
||||
this.discardButton = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
||||
label: loc.discardText,
|
||||
iconPath: IconPathHelper.discard,
|
||||
enabled: false
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.discardButton.onDidClick(async () => {
|
||||
this.discardButton!.enabled = false;
|
||||
try {
|
||||
this.editCores();
|
||||
this.editMemory();
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(loc.pageDiscardFailed(error));
|
||||
} finally {
|
||||
this.saveButton!.enabled = false;
|
||||
}
|
||||
}));
|
||||
|
||||
return this.modelView.modelBuilder.toolbarContainer().withToolbarItems([
|
||||
{ component: this.saveButton },
|
||||
{ component: this.discardButton }
|
||||
]).component();
|
||||
}
|
||||
|
||||
private initializeConfigurationBoxes() {
|
||||
this.coresLimitBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
readOnly: false,
|
||||
min: 1,
|
||||
inputType: 'number',
|
||||
placeHolder: loc.loading
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.coresLimitBox.onTextChanged(() => {
|
||||
if (!(this.handleOnTextChanged(this.coresLimitBox!))) {
|
||||
this.saveArgs.coresLimit = undefined;
|
||||
} else {
|
||||
this.saveArgs.coresLimit = this.coresLimitBox!.value;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.coresRequestBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
readOnly: false,
|
||||
min: 1,
|
||||
inputType: 'number',
|
||||
placeHolder: loc.loading
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.coresRequestBox.onTextChanged(() => {
|
||||
if (!(this.handleOnTextChanged(this.coresRequestBox!))) {
|
||||
this.saveArgs.coresRequest = undefined;
|
||||
} else {
|
||||
this.saveArgs.coresRequest = this.coresRequestBox!.value;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.memoryLimitBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
readOnly: false,
|
||||
min: 2,
|
||||
validationErrorMessage: loc.memoryLimitValidationErrorMessage,
|
||||
inputType: 'number',
|
||||
placeHolder: loc.loading
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.memoryLimitBox.onTextChanged(() => {
|
||||
if (!(this.handleOnTextChanged(this.memoryLimitBox!))) {
|
||||
this.saveArgs.memoryLimit = undefined;
|
||||
} else {
|
||||
this.saveArgs.memoryLimit = this.memoryLimitBox!.value + 'Gi';
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this.memoryRequestBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
readOnly: false,
|
||||
min: 2,
|
||||
validationErrorMessage: loc.memoryRequestValidationErrorMessage,
|
||||
inputType: 'number',
|
||||
placeHolder: loc.loading
|
||||
}).component();
|
||||
|
||||
this.disposables.push(
|
||||
this.memoryRequestBox.onTextChanged(() => {
|
||||
if (!(this.handleOnTextChanged(this.memoryRequestBox!))) {
|
||||
this.saveArgs.memoryRequest = undefined;
|
||||
} else {
|
||||
this.saveArgs.memoryRequest = this.memoryRequestBox!.value + 'Gi';
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
private createUserInputSection(): azdata.Component[] {
|
||||
if (this._miaaModel.configLastUpdated) {
|
||||
this.editCores();
|
||||
this.editMemory();
|
||||
}
|
||||
|
||||
return [
|
||||
this.createConfigurationSectionContainer(loc.coresRequest, this.coresRequestBox!),
|
||||
this.createConfigurationSectionContainer(loc.coresLimit, this.coresLimitBox!),
|
||||
this.createConfigurationSectionContainer(loc.memoryRequest, this.memoryRequestBox!),
|
||||
this.createConfigurationSectionContainer(loc.memoryLimit, this.memoryLimitBox!)
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
private createConfigurationSectionContainer(key: string, input: azdata.Component): azdata.FlexContainer {
|
||||
const inputFlex = { flex: '0 1 150px' };
|
||||
const keyFlex = { flex: `0 1 250px` };
|
||||
|
||||
const flexContainer = this.modelView.modelBuilder.flexContainer().withLayout({
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center'
|
||||
}).component();
|
||||
|
||||
const keyComponent = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: key,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component();
|
||||
|
||||
const keyContainer = this.modelView.modelBuilder.flexContainer().withLayout({ alignItems: 'center' }).component();
|
||||
keyContainer.addItem(keyComponent, { CSSStyles: { 'margin-right': '0px', 'margin-bottom': '15px' } });
|
||||
flexContainer.addItem(keyContainer, keyFlex);
|
||||
|
||||
const inputContainer = this.modelView.modelBuilder.flexContainer().withLayout({ alignItems: 'center' }).component();
|
||||
inputContainer.addItem(input, { CSSStyles: { 'margin-bottom': '15px', 'min-width': '50px', 'max-width': '225px' } });
|
||||
|
||||
flexContainer.addItem(inputContainer, inputFlex);
|
||||
|
||||
return flexContainer;
|
||||
}
|
||||
|
||||
private handleOnTextChanged(component: azdata.InputBoxComponent): boolean {
|
||||
if ((!component.value)) {
|
||||
// if there is no text found in the inputbox component return false
|
||||
return false;
|
||||
} else if ((!component.valid)) {
|
||||
// if value given by user is not valid enable discard button for user
|
||||
// to clear all inputs and return false
|
||||
this.discardButton!.enabled = true;
|
||||
return false;
|
||||
} else {
|
||||
// if a valid value has been entered into the input box, enable save and discard buttons
|
||||
// so that user could choose to either edit instance or clear all inputs
|
||||
// return true
|
||||
this.saveButton!.enabled = true;
|
||||
this.discardButton!.enabled = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private editCores(): void {
|
||||
let currentCPUSize = this._miaaModel.config?.spec?.requests?.vcores;
|
||||
|
||||
if (!currentCPUSize) {
|
||||
currentCPUSize = '';
|
||||
}
|
||||
|
||||
this.coresRequestBox!.validationErrorMessage = loc.validationMin(this.coresRequestBox!.min!);
|
||||
this.coresRequestBox!.placeHolder = currentCPUSize;
|
||||
this.coresRequestBox!.value = '';
|
||||
this.saveArgs.coresRequest = undefined;
|
||||
|
||||
currentCPUSize = this._miaaModel.config?.spec?.limits?.vcores;
|
||||
|
||||
if (!currentCPUSize) {
|
||||
currentCPUSize = '';
|
||||
}
|
||||
|
||||
this.coresLimitBox!.validationErrorMessage = loc.validationMin(this.coresLimitBox!.min!);
|
||||
this.coresLimitBox!.placeHolder = currentCPUSize;
|
||||
this.coresLimitBox!.value = '';
|
||||
this.saveArgs.coresLimit = undefined;
|
||||
}
|
||||
|
||||
private editMemory(): void {
|
||||
let currentMemSizeConversion: string;
|
||||
let currentMemorySize = this._miaaModel.config?.spec?.requests?.memory;
|
||||
|
||||
if (!currentMemorySize) {
|
||||
currentMemSizeConversion = '';
|
||||
} else {
|
||||
currentMemSizeConversion = convertToGibibyteString(currentMemorySize);
|
||||
}
|
||||
|
||||
this.memoryRequestBox!.placeHolder = currentMemSizeConversion!;
|
||||
this.memoryRequestBox!.value = '';
|
||||
|
||||
this.saveArgs.memoryRequest = undefined;
|
||||
|
||||
currentMemorySize = this._miaaModel.config?.spec?.limits?.memory;
|
||||
|
||||
if (!currentMemorySize) {
|
||||
currentMemSizeConversion = '';
|
||||
} else {
|
||||
currentMemSizeConversion = convertToGibibyteString(currentMemorySize);
|
||||
}
|
||||
|
||||
this.memoryLimitBox!.placeHolder = currentMemSizeConversion!;
|
||||
this.memoryLimitBox!.value = '';
|
||||
|
||||
this.saveArgs.memoryLimit = undefined;
|
||||
}
|
||||
|
||||
private handleServiceUpdated() {
|
||||
this.editCores();
|
||||
this.editMemory();
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { ControllerModel } from '../../../models/controllerModel';
|
||||
import * as loc from '../../../localizedConstants';
|
||||
import { MiaaConnectionStringsPage } from './miaaConnectionStringsPage';
|
||||
import { MiaaModel } from '../../../models/miaaModel';
|
||||
import { MiaaComputeAndStoragePage } from './miaaComputeAndStoragePage';
|
||||
|
||||
export class MiaaDashboard extends Dashboard {
|
||||
|
||||
@@ -27,12 +28,14 @@ export class MiaaDashboard extends Dashboard {
|
||||
protected async registerTabs(modelView: azdata.ModelView): Promise<(azdata.DashboardTab | azdata.DashboardTabGroup)[]> {
|
||||
const overviewPage = new MiaaDashboardOverviewPage(modelView, this._controllerModel, this._miaaModel);
|
||||
const connectionStringsPage = new MiaaConnectionStringsPage(modelView, this._controllerModel, this._miaaModel);
|
||||
const computeAndStoragePage = new MiaaComputeAndStoragePage(modelView, this._miaaModel);
|
||||
return [
|
||||
overviewPage.tab,
|
||||
{
|
||||
title: loc.settings,
|
||||
tabs: [
|
||||
connectionStringsPage.tab
|
||||
connectionStringsPage.tab,
|
||||
computeAndStoragePage.tab
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
@@ -67,11 +67,11 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
}).component());
|
||||
|
||||
const infoComputeStorage_p1 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: loc.computeAndStorageDescriptionPartOne,
|
||||
value: loc.postgresComputeAndStorageDescriptionPartOne,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px', 'max-width': 'auto' }
|
||||
}).component();
|
||||
const infoComputeStorage_p2 = this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
value: loc.computeAndStorageDescriptionPartTwo,
|
||||
value: loc.postgresComputeAndStorageDescriptionPartTwo,
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component();
|
||||
|
||||
@@ -107,15 +107,19 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
CSSStyles: { ...cssStyles.text, 'margin-block-start': '0px', 'margin-block-end': '0px' }
|
||||
}).component();
|
||||
|
||||
const computeInfoAndLinks = this.modelView.modelBuilder.flexContainer().withLayout({ flexWrap: 'wrap' }).component();
|
||||
computeInfoAndLinks.addItem(infoComputeStorage_p1, { CSSStyles: { 'margin-right': '5px' } });
|
||||
computeInfoAndLinks.addItem(infoComputeStorage_p2, { CSSStyles: { 'margin-right': '5px' } });
|
||||
computeInfoAndLinks.addItem(workerNodeslink, { CSSStyles: { 'margin-right': '5px' } });
|
||||
computeInfoAndLinks.addItem(infoComputeStorage_p3, { CSSStyles: { 'margin-right': '5px' } });
|
||||
computeInfoAndLinks.addItem(memoryVCoreslink, { CSSStyles: { 'margin-right': '5px' } });
|
||||
computeInfoAndLinks.addItem(infoComputeStorage_p4, { CSSStyles: { 'margin-right': '5px' } });
|
||||
computeInfoAndLinks.addItem(infoComputeStorage_p5, { CSSStyles: { 'margin-right': '5px' } });
|
||||
computeInfoAndLinks.addItem(infoComputeStorage_p6, { CSSStyles: { 'margin-right': '5px' } });
|
||||
const computeInfoAndLinks = this.modelView.modelBuilder.flexContainer()
|
||||
.withLayout({ flexWrap: 'wrap' })
|
||||
.withItems([
|
||||
infoComputeStorage_p1,
|
||||
infoComputeStorage_p2,
|
||||
workerNodeslink,
|
||||
infoComputeStorage_p3,
|
||||
memoryVCoreslink,
|
||||
infoComputeStorage_p4,
|
||||
infoComputeStorage_p5,
|
||||
infoComputeStorage_p6
|
||||
], { CSSStyles: { 'margin-right': '5px' } })
|
||||
.component();
|
||||
content.addItem(computeInfoAndLinks, { CSSStyles: { 'min-height': '30px' } });
|
||||
|
||||
content.addItem(this.modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||
@@ -151,8 +155,17 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
cancellable: false
|
||||
},
|
||||
async (_progress, _token): Promise<void> => {
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name, this.saveArgs);
|
||||
try {
|
||||
await this._azdataApi.azdata.arc.postgres.server.edit(
|
||||
this._postgresModel.info.name,
|
||||
this.saveArgs,
|
||||
this._postgresModel.engineVersion);
|
||||
} catch (err) {
|
||||
// If an error occurs while editing the instance then re-enable the save button since
|
||||
// the edit wasn't successfully applied
|
||||
this.saveButton!.enabled = true;
|
||||
throw err;
|
||||
}
|
||||
await this._postgresModel.refresh();
|
||||
}
|
||||
);
|
||||
@@ -214,7 +227,6 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
this.coresLimitBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
readOnly: false,
|
||||
min: 1,
|
||||
validationErrorMessage: loc.coresValidationErrorMessage,
|
||||
inputType: 'number',
|
||||
placeHolder: loc.loading
|
||||
}).component();
|
||||
@@ -232,7 +244,6 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
this.coresRequestBox = this.modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({
|
||||
readOnly: false,
|
||||
min: 1,
|
||||
validationErrorMessage: loc.coresValidationErrorMessage,
|
||||
inputType: 'number',
|
||||
placeHolder: loc.loading
|
||||
}).component();
|
||||
@@ -415,7 +426,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
|
||||
const information = this.modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({
|
||||
iconPath: IconPathHelper.information,
|
||||
title: loc.configurationInformation,
|
||||
title: loc.postgresConfigurationInformation,
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
enabled: false
|
||||
@@ -437,6 +448,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
currentCPUSize = '';
|
||||
}
|
||||
|
||||
this.coresRequestBox!.validationErrorMessage = loc.validationMin(this.coresRequestBox!.min!);
|
||||
this.coresRequestBox!.placeHolder = currentCPUSize;
|
||||
this.coresRequestBox!.value = '';
|
||||
this.saveArgs.coresRequest = undefined;
|
||||
@@ -447,6 +459,7 @@ export class PostgresComputeAndStoragePage extends DashboardPage {
|
||||
currentCPUSize = '';
|
||||
}
|
||||
|
||||
this.coresLimitBox!.validationErrorMessage = loc.validationMin(this.coresLimitBox!.min!);
|
||||
this.coresLimitBox!.placeHolder = currentCPUSize;
|
||||
this.coresLimitBox!.value = '';
|
||||
this.saveArgs.coresLimit = undefined;
|
||||
|
||||
@@ -157,6 +157,7 @@ export class PostgresOverviewPage extends DashboardPage {
|
||||
adminPassword: true,
|
||||
noWait: true
|
||||
},
|
||||
this._postgresModel.engineVersion,
|
||||
{ 'AZDATA_PASSWORD': password });
|
||||
vscode.window.showInformationMessage(loc.passwordReset);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { MiaaResourceInfo, ResourceInfo, ResourceType } from 'arc';
|
||||
import * as vscode from 'vscode';
|
||||
import { UserCancelledError } from '../../common/utils';
|
||||
import { UserCancelledError } from '../../common/api';
|
||||
import * as loc from '../../localizedConstants';
|
||||
import { ControllerModel, Registration } from '../../models/controllerModel';
|
||||
import { MiaaModel } from '../../models/miaaModel';
|
||||
|
||||
@@ -270,6 +270,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f"
|
||||
integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==
|
||||
|
||||
"@types/yamljs@^0.2.31":
|
||||
version "0.2.31"
|
||||
resolved "https://registry.yarnpkg.com/@types/yamljs/-/yamljs-0.2.31.tgz#b1a620b115c96db7b3bfdf0cf54aee0c57139245"
|
||||
integrity sha512-QcJ5ZczaXAqbVD3o8mw/mEBhRvO5UAdTtbvgwL/OgoWubvNBh6/MxLBAigtcgIFaq3shon9m3POIxQaLQt4fxQ==
|
||||
|
||||
ajv@^6.5.5:
|
||||
version "6.12.0"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7"
|
||||
@@ -299,6 +304,13 @@ append-transform@^2.0.0:
|
||||
dependencies:
|
||||
default-require-extensions "^3.0.0"
|
||||
|
||||
argparse@^1.0.7:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
|
||||
integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
|
||||
dependencies:
|
||||
sprintf-js "~1.0.2"
|
||||
|
||||
asn1@~0.2.3:
|
||||
version "0.2.4"
|
||||
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
|
||||
@@ -580,7 +592,7 @@ glob@7.1.2:
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^7.1.2, glob@^7.1.3:
|
||||
glob@^7.0.5, glob@^7.1.2, glob@^7.1.3:
|
||||
version "7.1.6"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
|
||||
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
|
||||
@@ -1109,6 +1121,11 @@ source-map@^0.6.1:
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
||||
|
||||
sprintf-js@~1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
|
||||
|
||||
sshpk@^1.7.0:
|
||||
version "1.16.1"
|
||||
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
|
||||
@@ -1251,3 +1268,11 @@ xml@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5"
|
||||
integrity sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=
|
||||
|
||||
yamljs@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/yamljs/-/yamljs-0.3.0.tgz#dc060bf267447b39f7304e9b2bfbe8b5a7ddb03b"
|
||||
integrity sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==
|
||||
dependencies:
|
||||
argparse "^1.0.7"
|
||||
glob "^7.0.5"
|
||||
|
||||
@@ -53,13 +53,7 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"import pandas,sys,os,json,html,getpass,time,ntpath,uuid\n",
|
||||
"pandas_version = pandas.__version__.split('.')\n",
|
||||
"pandas_major = int(pandas_version[0])\n",
|
||||
"pandas_minor = int(pandas_version[1])\n",
|
||||
"pandas_patch = int(pandas_version[2])\n",
|
||||
"if not (pandas_major > 0 or (pandas_major == 0 and pandas_minor > 24) or (pandas_major == 0 and pandas_minor == 24 and pandas_patch >= 2)):\n",
|
||||
" sys.exit('Please upgrade the Notebook dependency before you can proceed, you can do it by running the \"Reinstall Notebook dependencies\" command in command palette (View menu -> Command Palette…).')\n",
|
||||
"import sys,os,json,html,getpass,time,ntpath,uuid\n",
|
||||
"\n",
|
||||
"def run_command(command:str, displayCommand:str = \"\", returnObject:bool = False):\n",
|
||||
" print(\"Executing: \" + (displayCommand if displayCommand != \"\" else command))\n",
|
||||
|
||||
@@ -48,14 +48,8 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"import pandas,sys,os,getpass,json,html,time\n",
|
||||
"import sys,os,getpass,json,html,time\n",
|
||||
"from string import Template\n",
|
||||
"pandas_version = pandas.__version__.split('.')\n",
|
||||
"pandas_major = int(pandas_version[0])\n",
|
||||
"pandas_minor = int(pandas_version[1])\n",
|
||||
"pandas_patch = int(pandas_version[2])\n",
|
||||
"if not (pandas_major > 0 or (pandas_major == 0 and pandas_minor > 24) or (pandas_major == 0 and pandas_minor == 24 and pandas_patch >= 2)):\n",
|
||||
" sys.exit('Please upgrade the Notebook dependency before you can proceed, you can do it by running the \"Reinstall Notebook dependencies\" command in command palette (View menu -> Command Palette…).')\n",
|
||||
"\n",
|
||||
"def run_command(displayCommand = \"\"):\n",
|
||||
" print(\"Executing: \" + (displayCommand if displayCommand != \"\" else cmd))\n",
|
||||
|
||||
@@ -48,13 +48,7 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"import pandas,sys,os,json,html,getpass,time,ntpath,uuid\n",
|
||||
"pandas_version = pandas.__version__.split('.')\n",
|
||||
"pandas_major = int(pandas_version[0])\n",
|
||||
"pandas_minor = int(pandas_version[1])\n",
|
||||
"pandas_patch = int(pandas_version[2])\n",
|
||||
"if not (pandas_major > 0 or (pandas_major == 0 and pandas_minor > 24) or (pandas_major == 0 and pandas_minor == 24 and pandas_patch >= 2)):\n",
|
||||
" sys.exit('Please upgrade the Notebook dependency before you can proceed, you can do it by running the \"Reinstall Notebook dependencies\" command in command palette (View menu -> Command Palette…).')\n",
|
||||
"import sys,os,json,html,getpass,time,ntpath,uuid\n",
|
||||
"\n",
|
||||
"def run_command(command:str, displayCommand:str = \"\", returnObject:bool = False):\n",
|
||||
" print(\"Executing: \" + (displayCommand if displayCommand != \"\" else command))\n",
|
||||
|
||||
@@ -46,13 +46,7 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"import pandas,sys,getpass,os,json,html,time\n",
|
||||
"pandas_version = pandas.__version__.split('.')\n",
|
||||
"pandas_major = int(pandas_version[0])\n",
|
||||
"pandas_minor = int(pandas_version[1])\n",
|
||||
"pandas_patch = int(pandas_version[2])\n",
|
||||
"if not (pandas_major > 0 or (pandas_major == 0 and pandas_minor > 24) or (pandas_major == 0 and pandas_minor == 24 and pandas_patch >= 2)):\n",
|
||||
" sys.exit('Please upgrade the Notebook dependency before you can proceed, you can do it by running the \"Reinstall Notebook dependencies\" command in command palette (View menu -> Command Palette…).')\n",
|
||||
"import sys,getpass,os,json,html,time\n",
|
||||
"\n",
|
||||
"def run_command():\n",
|
||||
" print(\"Executing: \" + cmd)\n",
|
||||
|
||||
@@ -51,13 +51,7 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"import pandas,sys,os,json,html,getpass,time,ntpath,uuid\n",
|
||||
"pandas_version = pandas.__version__.split('.')\n",
|
||||
"pandas_major = int(pandas_version[0])\n",
|
||||
"pandas_minor = int(pandas_version[1])\n",
|
||||
"pandas_patch = int(pandas_version[2])\n",
|
||||
"if not (pandas_major > 0 or (pandas_major == 0 and pandas_minor > 24) or (pandas_major == 0 and pandas_minor == 24 and pandas_patch >= 2)):\n",
|
||||
" sys.exit('Please upgrade the Notebook dependency before you can proceed, you can do it by running the \"Reinstall Notebook dependencies\" command in command palette (View menu -> Command Palette…).')\n",
|
||||
"import sys,os,json,html,getpass,time,ntpath,uuid\n",
|
||||
"\n",
|
||||
"def run_command(command:str, displayCommand:str = \"\", returnObject:bool = False):\n",
|
||||
" print(\"Executing: \" + (displayCommand if displayCommand != \"\" else command))\n",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "asde-deployment",
|
||||
"displayName": "%extension-displayName%",
|
||||
"description": "%extension-description%",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.1",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
||||
@@ -296,7 +296,6 @@
|
||||
"defaultValue": "westus",
|
||||
"required": true,
|
||||
"locationVariableName": "AZDATA_NB_VAR_ASDE_AZURE_LOCATION",
|
||||
"displayLocationVariableName": "AZDATA_NB_VAR_ASDE_AZURE_LOCATION_TEXT",
|
||||
"locations": [
|
||||
"australiaeast",
|
||||
"australiasoutheast",
|
||||
@@ -339,9 +338,11 @@
|
||||
"confirmationRequired": true,
|
||||
"confirmationLabel": "%vm_password_confirm%",
|
||||
"required": true,
|
||||
"textValidationRequired": true,
|
||||
"textValidationRegex": "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[\\W_])[A-Za-z\\d\\W_]{12,123}$",
|
||||
"textValidationDescription": "%vm_password_validation_error_message%"
|
||||
"validations" : [{
|
||||
"type": "regex_match",
|
||||
"regex": "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[\\W_])[A-Za-z\\d\\W_]{12,123}$",
|
||||
"description": "%vm_password_validation_error_message%"
|
||||
}]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
"name": "azdata",
|
||||
"displayName": "%azdata.displayName%",
|
||||
"description": "%azdata.description%",
|
||||
"version": "0.4.1",
|
||||
"version": "0.5.0",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
||||
"icon": "images/extension.png",
|
||||
"engines": {
|
||||
"vscode": "*",
|
||||
"azdata": ">=1.23.0"
|
||||
"azdata": ">=1.25.0"
|
||||
},
|
||||
"activationEvents": [
|
||||
"*"
|
||||
|
||||
@@ -102,10 +102,11 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
|
||||
replaceEngineSettings?: boolean;
|
||||
workers?: number;
|
||||
},
|
||||
engineVersion?: string,
|
||||
additionalEnvVars?: { [key: string]: string; }) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.postgres.server.edit(name, args, additionalEnvVars);
|
||||
return azdataToolService.localAzdata.arc.postgres.server.edit(name, args, engineVersion, additionalEnvVars);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -125,6 +126,19 @@ export function getAzdataApi(localAzdataDiscovered: Promise<IAzdataTool | undefi
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.sql.mi.show(name);
|
||||
},
|
||||
edit: async (
|
||||
name: string,
|
||||
args: {
|
||||
coresLimit?: string;
|
||||
coresRequest?: string;
|
||||
memoryLimit?: string;
|
||||
memoryRequest?: string;
|
||||
noWait?: boolean;
|
||||
}) => {
|
||||
await localAzdataDiscovered;
|
||||
throwIfNoAzdataOrEulaNotAccepted(azdataToolService.localAzdata, isEulaAccepted(memento));
|
||||
return azdataToolService.localAzdata.arc.sql.mi.edit(name, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,19 +118,21 @@ export class AzdataTool implements azdataExt.IAzdataApi {
|
||||
replaceEngineSettings?: boolean,
|
||||
workers?: number
|
||||
},
|
||||
engineVersion?: string,
|
||||
additionalEnvVars?: { [key: string]: string }): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
const argsArray = ['arc', 'postgres', 'server', 'edit', '-n', name];
|
||||
if (args.adminPassword) { argsArray.push('--admin-password'); }
|
||||
if (args.coresLimit !== undefined) { argsArray.push('--cores-limit', args.coresLimit); }
|
||||
if (args.coresRequest !== undefined) { argsArray.push('--cores-request', args.coresRequest); }
|
||||
if (args.engineSettings !== undefined) { argsArray.push('--engine-settings', args.engineSettings); }
|
||||
if (args.extensions !== undefined) { argsArray.push('--extensions', args.extensions); }
|
||||
if (args.memoryLimit !== undefined) { argsArray.push('--memory-limit', args.memoryLimit); }
|
||||
if (args.memoryRequest !== undefined) { argsArray.push('--memory-request', args.memoryRequest); }
|
||||
if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); }
|
||||
if (args.coresRequest) { argsArray.push('--cores-request', args.coresRequest); }
|
||||
if (args.engineSettings) { argsArray.push('--engine-settings', args.engineSettings); }
|
||||
if (args.extensions) { argsArray.push('--extensions', args.extensions); }
|
||||
if (args.memoryLimit) { argsArray.push('--memory-limit', args.memoryLimit); }
|
||||
if (args.memoryRequest) { argsArray.push('--memory-request', args.memoryRequest); }
|
||||
if (args.noWait) { argsArray.push('--no-wait'); }
|
||||
if (args.port !== undefined) { argsArray.push('--port', args.port.toString()); }
|
||||
if (args.port) { argsArray.push('--port', args.port.toString()); }
|
||||
if (args.replaceEngineSettings) { argsArray.push('--replace-engine-settings'); }
|
||||
if (args.workers !== undefined) { argsArray.push('--workers', args.workers.toString()); }
|
||||
if (args.workers) { argsArray.push('--workers', args.workers.toString()); }
|
||||
if (engineVersion) { argsArray.push('--engine-version', engineVersion); }
|
||||
return this.executeCommand<void>(argsArray, additionalEnvVars);
|
||||
}
|
||||
}
|
||||
@@ -145,6 +147,23 @@ export class AzdataTool implements azdataExt.IAzdataApi {
|
||||
},
|
||||
show: (name: string): Promise<azdataExt.AzdataOutput<azdataExt.SqlMiShowResult>> => {
|
||||
return this.executeCommand<azdataExt.SqlMiShowResult>(['arc', 'sql', 'mi', 'show', '-n', name]);
|
||||
},
|
||||
edit: (
|
||||
name: string,
|
||||
args: {
|
||||
coresLimit?: string,
|
||||
coresRequest?: string,
|
||||
memoryLimit?: string,
|
||||
memoryRequest?: string,
|
||||
noWait?: boolean,
|
||||
}): Promise<azdataExt.AzdataOutput<void>> => {
|
||||
const argsArray = ['arc', 'sql', 'mi', 'edit', '-n', name];
|
||||
if (args.coresLimit) { argsArray.push('--cores-limit', args.coresLimit); }
|
||||
if (args.coresRequest) { argsArray.push('--cores-request', args.coresRequest); }
|
||||
if (args.memoryLimit) { argsArray.push('--memory-limit', args.memoryLimit); }
|
||||
if (args.memoryRequest) { argsArray.push('--memory-request', args.memoryRequest); }
|
||||
if (args.noWait) { argsArray.push('--no-wait'); }
|
||||
return this.executeCommand<void>(argsArray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<azdata
|
||||
|
||||
// register option source(s)
|
||||
const rdApi = <rd.IExtension>vscode.extensions.getExtension(rd.extension.name)?.exports;
|
||||
rdApi.registerOptionsSourceProvider(new ArcControllerConfigProfilesOptionsSource(azdataApi));
|
||||
context.subscriptions.push(rdApi.registerOptionsSourceProvider(new ArcControllerConfigProfilesOptionsSource(azdataApi)));
|
||||
|
||||
return azdataApi;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as azdataExt from 'azdata-ext';
|
||||
* Class that provides options sources for an Arc Data Controller
|
||||
*/
|
||||
export class ArcControllerConfigProfilesOptionsSource implements rd.IOptionsSourceProvider {
|
||||
readonly optionsSourceId = 'arc.controller.config.profiles';
|
||||
readonly id = 'arc.controller.config.profiles';
|
||||
constructor(private _azdataExtApi: azdataExt.IExtension) { }
|
||||
async getOptions(): Promise<string[]> {
|
||||
const isEulaAccepted = await this._azdataExtApi.isEulaAccepted();
|
||||
|
||||
20
extensions/azdata/src/typings/azdata-ext.d.ts
vendored
20
extensions/azdata/src/typings/azdata-ext.d.ts
vendored
@@ -125,7 +125,12 @@ declare module 'azdata-ext' {
|
||||
},
|
||||
spec: {
|
||||
limits?: {
|
||||
vcores?: number // 4
|
||||
memory?: string // "10Gi"
|
||||
vcores?: string // "4"
|
||||
},
|
||||
requests?: {
|
||||
memory?: string // "10Gi"
|
||||
vcores?: string // "4"
|
||||
}
|
||||
service: {
|
||||
type: string // "NodePort"
|
||||
@@ -257,6 +262,7 @@ declare module 'azdata-ext' {
|
||||
replaceEngineSettings?: boolean,
|
||||
workers?: number
|
||||
},
|
||||
engineVersion?: string,
|
||||
additionalEnvVars?: { [key: string]: string }): Promise<AzdataOutput<void>>
|
||||
}
|
||||
},
|
||||
@@ -264,7 +270,17 @@ declare module 'azdata-ext' {
|
||||
mi: {
|
||||
delete(name: string): Promise<AzdataOutput<void>>,
|
||||
list(): Promise<AzdataOutput<SqlMiListResult[]>>,
|
||||
show(name: string): Promise<AzdataOutput<SqlMiShowResult>>
|
||||
show(name: string): Promise<AzdataOutput<SqlMiShowResult>>,
|
||||
edit(
|
||||
name: string,
|
||||
args: {
|
||||
coresLimit?: string,
|
||||
coresRequest?: string,
|
||||
memoryLimit?: string,
|
||||
memoryRequest?: string,
|
||||
noWait?: boolean,
|
||||
}
|
||||
): Promise<AzdataOutput<void>>
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -141,7 +141,12 @@
|
||||
"icon": "$(refresh)"
|
||||
},
|
||||
{
|
||||
"command": "azure.resource.refresh",
|
||||
"command": "azure.resource.azureview.refresh",
|
||||
"title": "%azure.resource.refresh.title%",
|
||||
"icon": "$(refresh)"
|
||||
},
|
||||
{
|
||||
"command": "azure.resource.connectiondialog.refresh",
|
||||
"title": "%azure.resource.refresh.title%",
|
||||
"icon": "$(refresh)"
|
||||
},
|
||||
@@ -209,7 +214,11 @@
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "azure.resource.refresh",
|
||||
"command": "azure.resource.azureview.refresh",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "azure.resource.connectiondialog.refresh",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
@@ -245,12 +254,12 @@
|
||||
"group": "azurecore"
|
||||
},
|
||||
{
|
||||
"command": "azure.resource.refresh",
|
||||
"command": "azure.resource.azureview.refresh",
|
||||
"when": "viewItem =~ /^azure\\.resource\\.itemType\\.(?:account|subscription|databaseContainer|databaseServerContainer)$/",
|
||||
"group": "inline"
|
||||
},
|
||||
{
|
||||
"command": "azure.resource.refresh",
|
||||
"command": "azure.resource.azureview.refresh",
|
||||
"when": "viewItem =~ /^azure\\.resource\\.itemType\\.(?:account|subscription|databaseContainer|databaseServerContainer)$/",
|
||||
"group": "azurecore"
|
||||
},
|
||||
@@ -287,7 +296,7 @@
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "azure.resource.refresh",
|
||||
"command": "azure.resource.connectiondialog.refresh",
|
||||
"when": "contextValue == azure.resource.itemType.account",
|
||||
"group": "navigation"
|
||||
},
|
||||
@@ -312,6 +321,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/arm-resourcegraph": "^2.0.0",
|
||||
"@azure/arm-storage": "^15.1.0",
|
||||
"@azure/arm-subscriptions": "1.0.0",
|
||||
"axios": "^0.19.2",
|
||||
"qs": "^6.9.1",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
declare module 'azureResource' {
|
||||
import { TreeDataProvider } from 'vscode';
|
||||
import { DataProvider, Account, TreeItem } from 'azdata';
|
||||
import { FileShareItem, ListContainerItem } from '@azure/arm-storage/esm/models';
|
||||
export namespace azureResource {
|
||||
|
||||
export const enum AzureResourceType {
|
||||
@@ -18,7 +19,8 @@ declare module 'azureResource' {
|
||||
kustoClusters = 'microsoft.kusto/clusters',
|
||||
azureArcPostgresServer = 'microsoft.azuredata/postgresinstances',
|
||||
postgresServer = 'microsoft.dbforpostgresql/servers',
|
||||
azureArcService = 'microsoft.azuredata/datacontrollers'
|
||||
azureArcService = 'microsoft.azuredata/datacontrollers',
|
||||
storageAccount = 'microsoft.storage/storageaccounts',
|
||||
}
|
||||
|
||||
export interface IAzureResourceProvider extends DataProvider {
|
||||
@@ -75,7 +77,10 @@ declare module 'azureResource' {
|
||||
fullName: string;
|
||||
defaultDatabaseName: string;
|
||||
}
|
||||
export interface BlobContainer extends ListContainerItem {
|
||||
}
|
||||
|
||||
|
||||
export interface FileShare extends FileShareItem {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,8 @@ import { AzureAccount, Tenant } from '../account-provider/interfaces';
|
||||
import { FlatAccountTreeNode } from './tree/flatAccountTreeNode';
|
||||
import { ConnectionDialogTreeProvider } from './tree/connectionDialogTreeProvider';
|
||||
|
||||
export function registerAzureResourceCommands(appContext: AppContext, trees: (AzureResourceTreeProvider | ConnectionDialogTreeProvider)[]): void {
|
||||
export function registerAzureResourceCommands(appContext: AppContext, azureViewTree: AzureResourceTreeProvider, connectionDialogTree: ConnectionDialogTreeProvider): void {
|
||||
const trees = [azureViewTree, connectionDialogTree];
|
||||
vscode.commands.registerCommand('azure.resource.startterminal', async (node?: TreeNode) => {
|
||||
try {
|
||||
const enablePreviewFeatures = vscode.workspace.getConfiguration('workbench').get('enablePreviewFeatures');
|
||||
@@ -168,10 +169,12 @@ export function registerAzureResourceCommands(appContext: AppContext, trees: (Az
|
||||
}
|
||||
});
|
||||
|
||||
vscode.commands.registerCommand('azure.resource.refresh', async (node?: TreeNode) => {
|
||||
for (const tree of trees) {
|
||||
await tree.refresh(node, true);
|
||||
}
|
||||
vscode.commands.registerCommand('azure.resource.azureview.refresh', async (node?: TreeNode) => {
|
||||
await azureViewTree.refresh(node, true);
|
||||
});
|
||||
|
||||
vscode.commands.registerCommand('azure.resource.connectiondialog.refresh', async (node?: TreeNode) => {
|
||||
await connectionDialogTree.refresh(node, true);
|
||||
});
|
||||
|
||||
vscode.commands.registerCommand('azure.resource.signin', async (node?: TreeNode) => {
|
||||
|
||||
@@ -18,7 +18,7 @@ import { AzureResourceContainerTreeNodeBase } from './baseTreeNodes';
|
||||
import { AzureResourceItemType, AzureResourceServiceNames } from '../constants';
|
||||
import { AzureResourceMessageTreeNode } from '../messageTreeNode';
|
||||
import { IAzureResourceTreeChangeHandler } from './treeChangeHandler';
|
||||
import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService, IAzureResourceNodeWithProviderId } from '../../azureResource/interfaces';
|
||||
import { IAzureResourceSubscriptionService, IAzureResourceSubscriptionFilterService } from '../../azureResource/interfaces';
|
||||
import { AzureAccount } from '../../account-provider/interfaces';
|
||||
import { AzureResourceService } from '../resourceService';
|
||||
import { AzureResourceResourceTreeNode } from '../resourceTreeNode';
|
||||
@@ -39,11 +39,22 @@ export class FlatAccountTreeNode extends AzureResourceContainerTreeNodeBase {
|
||||
this._id = `account_${this.account.key.accountId}`;
|
||||
this.setCacheKey(`${this._id}.dataresources`);
|
||||
this._label = account.displayInfo.displayName;
|
||||
this._loader = new FlatAccountTreeNodeLoader(appContext, this._resourceService, this._subscriptionService, this._subscriptionFilterService, this.account, this);
|
||||
this._loader.onNewResourcesAvailable(() => {
|
||||
this.treeChangeHandler.notifyNodeChanged(this);
|
||||
});
|
||||
|
||||
this._loader.onLoadingStatusChanged(async () => {
|
||||
await this.updateLabel();
|
||||
this.treeChangeHandler.notifyNodeChanged(this);
|
||||
});
|
||||
}
|
||||
|
||||
public async updateLabel(): Promise<void> {
|
||||
const subscriptionInfo = await this.getSubscriptionInfo();
|
||||
if (subscriptionInfo.total !== 0) {
|
||||
const subscriptionInfo = await getSubscriptionInfo(this.account, this._subscriptionService, this._subscriptionFilterService);
|
||||
if (this._loader.isLoading) {
|
||||
this._label = localize('azure.resource.tree.accountTreeNode.titleLoading', "{0} - Loading...", this.account.displayInfo.displayName);
|
||||
} else if (subscriptionInfo.total !== 0) {
|
||||
this._label = localize({
|
||||
key: 'azure.resource.tree.accountTreeNode.title',
|
||||
comment: [
|
||||
@@ -57,79 +68,13 @@ export class FlatAccountTreeNode extends AzureResourceContainerTreeNodeBase {
|
||||
}
|
||||
}
|
||||
|
||||
private async getSubscriptionInfo(): Promise<{
|
||||
subscriptions: azureResource.AzureResourceSubscription[],
|
||||
total: number,
|
||||
selected: number
|
||||
}> {
|
||||
let subscriptions: azureResource.AzureResourceSubscription[] = [];
|
||||
try {
|
||||
for (const tenant of this.account.properties.tenants) {
|
||||
const token = await azdata.accounts.getAccountSecurityToken(this.account, tenant.id, azdata.AzureResource.ResourceManagement);
|
||||
|
||||
subscriptions.push(...(await this._subscriptionService.getSubscriptions(this.account, new TokenCredentials(token.token, token.tokenType), tenant.id) || <azureResource.AzureResourceSubscription[]>[]));
|
||||
}
|
||||
} catch (error) {
|
||||
throw new AzureResourceCredentialError(localize('azure.resource.tree.accountTreeNode.credentialError', "Failed to get credential for account {0}. Please refresh the account.", this.account.key.accountId), error);
|
||||
}
|
||||
const total = subscriptions.length;
|
||||
let selected = total;
|
||||
|
||||
const selectedSubscriptions = await this._subscriptionFilterService.getSelectedSubscriptions(this.account);
|
||||
const selectedSubscriptionIds = (selectedSubscriptions || <azureResource.AzureResourceSubscription[]>[]).map((subscription) => subscription.id);
|
||||
if (selectedSubscriptionIds.length > 0) {
|
||||
subscriptions = subscriptions.filter((subscription) => selectedSubscriptionIds.indexOf(subscription.id) !== -1);
|
||||
selected = selectedSubscriptionIds.length;
|
||||
}
|
||||
return {
|
||||
subscriptions,
|
||||
total,
|
||||
selected
|
||||
};
|
||||
}
|
||||
|
||||
public async getChildren(): Promise<TreeNode[]> {
|
||||
try {
|
||||
let dataResources: IAzureResourceNodeWithProviderId[] = [];
|
||||
if (this._isClearingCache) {
|
||||
let subscriptions: azureResource.AzureResourceSubscription[] = (await this.getSubscriptionInfo()).subscriptions;
|
||||
|
||||
if (subscriptions.length === 0) {
|
||||
return [AzureResourceMessageTreeNode.create(FlatAccountTreeNode.noSubscriptionsLabel, this)];
|
||||
} else {
|
||||
// Filter out everything that we can't authenticate to.
|
||||
subscriptions = subscriptions.filter(async s => {
|
||||
const token = await azdata.accounts.getAccountSecurityToken(this.account, s.tenant, azdata.AzureResource.ResourceManagement);
|
||||
if (!token) {
|
||||
console.info(`Account does not have permissions to view subscription ${JSON.stringify(s)}.`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
const resourceProviderIds = await this._resourceService.listResourceProviderIds();
|
||||
for (const subscription of subscriptions) {
|
||||
for (const providerId of resourceProviderIds) {
|
||||
const resourceTypes = await this._resourceService.getRootChildren(providerId, this.account, subscription, subscription.tenant);
|
||||
for (const resourceType of resourceTypes) {
|
||||
dataResources.push(...await this._resourceService.getChildren(providerId, resourceType.resourceNode, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
dataResources = dataResources.sort((a, b) => { return a.resourceNode.treeItem.label.localeCompare(b.resourceNode.treeItem.label); });
|
||||
this.updateCache(dataResources);
|
||||
this._isClearingCache = false;
|
||||
} else {
|
||||
dataResources = this.getCache<IAzureResourceNodeWithProviderId[]>();
|
||||
}
|
||||
|
||||
return dataResources.map(dr => new AzureResourceResourceTreeNode(dr, this, this.appContext));
|
||||
} catch (error) {
|
||||
if (error instanceof AzureResourceCredentialError) {
|
||||
vscode.commands.executeCommand('azure.resource.signin');
|
||||
}
|
||||
return [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this)];
|
||||
if (this._isClearingCache) {
|
||||
this._loader.start();
|
||||
this._isClearingCache = false;
|
||||
return [];
|
||||
} else {
|
||||
return this._loader.nodes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,12 +107,126 @@ export class FlatAccountTreeNode extends AzureResourceContainerTreeNodeBase {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
private _subscriptionService: IAzureResourceSubscriptionService = undefined;
|
||||
private _subscriptionFilterService: IAzureResourceSubscriptionFilterService = undefined;
|
||||
private _resourceService: AzureResourceService = undefined;
|
||||
|
||||
private _id: string = undefined;
|
||||
private _label: string = undefined;
|
||||
|
||||
private static readonly noSubscriptionsLabel = localize('azure.resource.tree.accountTreeNode.noSubscriptionsLabel', "No Subscriptions found.");
|
||||
private _subscriptionService: IAzureResourceSubscriptionService;
|
||||
private _subscriptionFilterService: IAzureResourceSubscriptionFilterService;
|
||||
private _resourceService: AzureResourceService;
|
||||
private _loader: FlatAccountTreeNodeLoader;
|
||||
private _id: string;
|
||||
private _label: string;
|
||||
}
|
||||
|
||||
async function getSubscriptionInfo(account: AzureAccount, subscriptionService: IAzureResourceSubscriptionService, subscriptionFilterService: IAzureResourceSubscriptionFilterService): Promise<{
|
||||
subscriptions: azureResource.AzureResourceSubscription[],
|
||||
total: number,
|
||||
selected: number
|
||||
}> {
|
||||
let subscriptions: azureResource.AzureResourceSubscription[] = [];
|
||||
try {
|
||||
for (const tenant of account.properties.tenants) {
|
||||
const token = await azdata.accounts.getAccountSecurityToken(account, tenant.id, azdata.AzureResource.ResourceManagement);
|
||||
subscriptions.push(...(await subscriptionService.getSubscriptions(account, new TokenCredentials(token.token, token.tokenType), tenant.id) || <azureResource.AzureResourceSubscription[]>[]));
|
||||
}
|
||||
} catch (error) {
|
||||
throw new AzureResourceCredentialError(localize('azure.resource.tree.accountTreeNode.credentialError', "Failed to get credential for account {0}. Please refresh the account.", account.key.accountId), error);
|
||||
}
|
||||
const total = subscriptions.length;
|
||||
let selected = total;
|
||||
|
||||
const selectedSubscriptions = await subscriptionFilterService.getSelectedSubscriptions(account);
|
||||
const selectedSubscriptionIds = (selectedSubscriptions || <azureResource.AzureResourceSubscription[]>[]).map((subscription) => subscription.id);
|
||||
if (selectedSubscriptionIds.length > 0) {
|
||||
subscriptions = subscriptions.filter((subscription) => selectedSubscriptionIds.indexOf(subscription.id) !== -1);
|
||||
selected = selectedSubscriptionIds.length;
|
||||
}
|
||||
return {
|
||||
subscriptions,
|
||||
total,
|
||||
selected
|
||||
};
|
||||
}
|
||||
class FlatAccountTreeNodeLoader {
|
||||
|
||||
private _isLoading: boolean = false;
|
||||
private _nodes: TreeNode[];
|
||||
private readonly _onNewResourcesAvailable = new vscode.EventEmitter<void>();
|
||||
public readonly onNewResourcesAvailable = this._onNewResourcesAvailable.event;
|
||||
private readonly _onLoadingStatusChanged = new vscode.EventEmitter<void>();
|
||||
public readonly onLoadingStatusChanged = this._onLoadingStatusChanged.event;
|
||||
|
||||
constructor(private readonly appContext: AppContext,
|
||||
private readonly _resourceService: AzureResourceService,
|
||||
private readonly _subscriptionService: IAzureResourceSubscriptionService,
|
||||
private readonly _subscriptionFilterService: IAzureResourceSubscriptionFilterService,
|
||||
private readonly _account: AzureAccount,
|
||||
private readonly _accountNode: TreeNode) {
|
||||
}
|
||||
|
||||
public get isLoading(): boolean {
|
||||
return this._isLoading;
|
||||
}
|
||||
|
||||
public get nodes(): TreeNode[] {
|
||||
return this._nodes;
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
if (this._isLoading) {
|
||||
return;
|
||||
}
|
||||
this._isLoading = true;
|
||||
this._nodes = [];
|
||||
this._onLoadingStatusChanged.fire();
|
||||
let newNodesAvailable = false;
|
||||
|
||||
// Throttle the refresh events to at most once per 500ms
|
||||
const refreshHandle = setInterval(() => {
|
||||
if (newNodesAvailable) {
|
||||
this._onNewResourcesAvailable.fire();
|
||||
newNodesAvailable = false;
|
||||
}
|
||||
if (!this.isLoading) {
|
||||
clearInterval(refreshHandle);
|
||||
}
|
||||
}, 500);
|
||||
try {
|
||||
let subscriptions: azureResource.AzureResourceSubscription[] = (await getSubscriptionInfo(this._account, this._subscriptionService, this._subscriptionFilterService)).subscriptions;
|
||||
|
||||
if (subscriptions.length !== 0) {
|
||||
// Filter out everything that we can't authenticate to.
|
||||
subscriptions = subscriptions.filter(async s => {
|
||||
const token = await azdata.accounts.getAccountSecurityToken(this._account, s.tenant, azdata.AzureResource.ResourceManagement);
|
||||
if (!token) {
|
||||
console.info(`Account does not have permissions to view subscription ${JSON.stringify(s)}.`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
const resourceProviderIds = await this._resourceService.listResourceProviderIds();
|
||||
for (const subscription of subscriptions) {
|
||||
for (const providerId of resourceProviderIds) {
|
||||
const resourceTypes = await this._resourceService.getRootChildren(providerId, this._account, subscription, subscription.tenant);
|
||||
for (const resourceType of resourceTypes) {
|
||||
const resources = await this._resourceService.getChildren(providerId, resourceType.resourceNode, true);
|
||||
if (resources.length > 0) {
|
||||
this._nodes.push(...resources.map(dr => new AzureResourceResourceTreeNode(dr, this._accountNode, this.appContext)));
|
||||
this._nodes = this.nodes.sort((a, b) => {
|
||||
return a.getNodeInfo().label.localeCompare(b.getNodeInfo().label);
|
||||
});
|
||||
newNodesAvailable = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof AzureResourceCredentialError) {
|
||||
vscode.commands.executeCommand('azure.resource.signin');
|
||||
}
|
||||
this._nodes = [AzureResourceMessageTreeNode.create(AzureResourceErrorMessageUtil.getErrorMessage(error), this._accountNode)];
|
||||
}
|
||||
|
||||
this._isLoading = false;
|
||||
this._onLoadingStatusChanged.fire();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,18 @@
|
||||
|
||||
import { ResourceGraphClient } from '@azure/arm-resourcegraph';
|
||||
import { TokenCredentials } from '@azure/ms-rest-js';
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import * as azdata from 'azdata';
|
||||
import { GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult } from 'azurecore';
|
||||
import { HttpGetRequestResult, GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult, GetBlobContainersResult, GetFileSharesResult } from 'azurecore';
|
||||
import { azureResource } from 'azureResource';
|
||||
import { EOL } from 'os';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { AppContext } from '../appContext';
|
||||
import { invalidAzureAccount, invalidTenant, unableToFetchTokenError } from '../localizedConstants';
|
||||
import { AzureResourceServiceNames } from './constants';
|
||||
import { IAzureResourceSubscriptionFilterService, IAzureResourceSubscriptionService } from './interfaces';
|
||||
import { AzureResourceGroupService } from './providers/resourceGroup/resourceGroupService';
|
||||
import { StorageManagementClient } from '@azure/arm-storage';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
@@ -106,7 +110,7 @@ export function equals(one: any, other: any): boolean {
|
||||
export async function getResourceGroups(appContext: AppContext, account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors: boolean = false): Promise<GetResourceGroupsResult> {
|
||||
const result: GetResourceGroupsResult = { resourceGroups: [], errors: [] };
|
||||
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants) || !subscription) {
|
||||
const error = new Error(localize('azure.accounts.getResourceGroups.invalidParamsError', "Invalid account or subscription"));
|
||||
const error = new Error(invalidAzureAccount);
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
@@ -146,7 +150,7 @@ export async function runResourceQuery<T extends azureResource.AzureGraphResourc
|
||||
query: string): Promise<ResourceQueryResult<T>> {
|
||||
const result: ResourceQueryResult<T> = { resources: [], errors: [] };
|
||||
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
|
||||
const error = new Error(localize('azure.accounts.runResourceQuery.errors.invalidAccount', "Invalid account"));
|
||||
const error = new Error(invalidAzureAccount);
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
@@ -157,7 +161,7 @@ export async function runResourceQuery<T extends azureResource.AzureGraphResourc
|
||||
// Check our subscriptions to ensure we have valid ones
|
||||
subscriptions.forEach(subscription => {
|
||||
if (!subscription.tenant) {
|
||||
const error = new Error(localize('azure.accounts.runResourceQuery.errors.noTenantSpecifiedForSubscription', "Invalid tenant for subscription"));
|
||||
const error = new Error(invalidTenant);
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
@@ -188,7 +192,7 @@ export async function runResourceQuery<T extends azureResource.AzureGraphResourc
|
||||
resourceClient = new ResourceGraphClient(credential, { baseUri: account.properties.providerSettings.settings.armResource.endpoint });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = new Error(localize('azure.accounts.runResourceQuery.errors.unableToFetchToken', "Unable to get token for tenant {0}", tenant.id));
|
||||
const error = new Error(unableToFetchTokenError(tenant.id));
|
||||
result.errors.push(error);
|
||||
continue;
|
||||
}
|
||||
@@ -227,7 +231,7 @@ export async function runResourceQuery<T extends azureResource.AzureGraphResourc
|
||||
export async function getSubscriptions(appContext: AppContext, account?: azdata.Account, ignoreErrors: boolean = false): Promise<GetSubscriptionsResult> {
|
||||
const result: GetSubscriptionsResult = { subscriptions: [], errors: [] };
|
||||
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
|
||||
const error = new Error(localize('azure.accounts.getSubscriptions.invalidParamsError', "Invalid account"));
|
||||
const error = new Error(invalidAzureAccount);
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
@@ -261,7 +265,7 @@ export async function getSubscriptions(appContext: AppContext, account?: azdata.
|
||||
export async function getSelectedSubscriptions(appContext: AppContext, account?: azdata.Account, ignoreErrors: boolean = false): Promise<GetSubscriptionsResult> {
|
||||
const result: GetSubscriptionsResult = { subscriptions: [], errors: [] };
|
||||
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
|
||||
const error = new Error(localize('azure.accounts.getSelectedSubscriptions.invalidParamsError', "Invalid account"));
|
||||
const error = new Error(invalidAzureAccount);
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
@@ -284,3 +288,189 @@ export async function getSelectedSubscriptions(appContext: AppContext, account?:
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* makes a GET request to Azure REST apis. Currently, it only supports GET ARM queries.
|
||||
*/
|
||||
export async function makeHttpGetRequest(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, ignoreErrors: boolean = false, url: string): Promise<HttpGetRequestResult> {
|
||||
const result: HttpGetRequestResult = { response: {}, errors: [] };
|
||||
|
||||
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
|
||||
const error = new Error(invalidAzureAccount);
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
result.errors.push(error);
|
||||
}
|
||||
|
||||
if (!subscription.tenant) {
|
||||
const error = new Error(invalidTenant);
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
result.errors.push(error);
|
||||
}
|
||||
if (result.errors.length > 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
let securityToken: { token: string, tokenType?: string };
|
||||
try {
|
||||
securityToken = await azdata.accounts.getAccountSecurityToken(
|
||||
account,
|
||||
subscription.tenant!,
|
||||
azdata.AzureResource.ResourceManagement
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = new Error(unableToFetchTokenError(subscription.tenant));
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
result.errors.push(error);
|
||||
}
|
||||
if (result.errors.length > 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const config: AxiosRequestConfig = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${securityToken.token}`
|
||||
},
|
||||
validateStatus: () => true // Never throw
|
||||
};
|
||||
|
||||
const response = await axios.get(url, config);
|
||||
|
||||
if (response.status !== 200) {
|
||||
let errorMessage: string[] = [];
|
||||
errorMessage.push(response.status.toString());
|
||||
errorMessage.push(response.statusText);
|
||||
if (response.data && response.data.error) {
|
||||
errorMessage.push(`${response.data.error.code} : ${response.data.error.message}`);
|
||||
}
|
||||
const error = new Error(errorMessage.join(EOL));
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
result.errors.push(error);
|
||||
}
|
||||
|
||||
result.response = response;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getBlobContainers(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccounts: azureResource.AzureGraphResource, ignoreErrors: boolean): Promise<GetBlobContainersResult> {
|
||||
let result: GetBlobContainersResult = { blobContainer: undefined, errors: [] };
|
||||
|
||||
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
|
||||
const error = new Error(invalidAzureAccount);
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
result.errors.push(error);
|
||||
}
|
||||
|
||||
if (!subscription.tenant) {
|
||||
const error = new Error(invalidTenant);
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
result.errors.push(error);
|
||||
}
|
||||
if (result.errors.length > 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
let securityToken: { token: string, tokenType?: string };
|
||||
let credential: TokenCredentials;
|
||||
try {
|
||||
securityToken = await azdata.accounts.getAccountSecurityToken(
|
||||
account,
|
||||
subscription.tenant!,
|
||||
azdata.AzureResource.ResourceManagement
|
||||
);
|
||||
const token = securityToken.token;
|
||||
const tokenType = securityToken.tokenType;
|
||||
credential = new TokenCredentials(token, tokenType);
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = new Error(unableToFetchTokenError(subscription.tenant));
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
result.errors.push(error);
|
||||
}
|
||||
|
||||
try {
|
||||
const client = new StorageManagementClient(<any>credential, subscription.id);
|
||||
result.blobContainer = await client.blobContainers.list(storageAccounts.resourceGroup, storageAccounts.name);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (!ignoreErrors) {
|
||||
throw err;
|
||||
}
|
||||
result.errors.push(err);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getFileShares(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccounts: azureResource.AzureGraphResource, ignoreErrors: boolean): Promise<GetFileSharesResult> {
|
||||
let result: GetFileSharesResult = { fileShares: undefined, errors: [] };
|
||||
|
||||
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
|
||||
const error = new Error(invalidAzureAccount);
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
result.errors.push(error);
|
||||
}
|
||||
|
||||
if (!subscription.tenant) {
|
||||
const error = new Error(invalidTenant);
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
result.errors.push(error);
|
||||
}
|
||||
if (result.errors.length > 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
let securityToken: { token: string, tokenType?: string };
|
||||
let credential: TokenCredentials;
|
||||
try {
|
||||
securityToken = await azdata.accounts.getAccountSecurityToken(
|
||||
account,
|
||||
subscription.tenant!,
|
||||
azdata.AzureResource.ResourceManagement
|
||||
);
|
||||
const token = securityToken.token;
|
||||
const tokenType = securityToken.tokenType;
|
||||
credential = new TokenCredentials(token, tokenType);
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = new Error(unableToFetchTokenError(subscription.tenant));
|
||||
if (!ignoreErrors) {
|
||||
throw error;
|
||||
}
|
||||
result.errors.push(error);
|
||||
}
|
||||
|
||||
try {
|
||||
const client = new StorageManagementClient(<any>credential, subscription.id);
|
||||
result.fileShares = await client.fileShares.list(storageAccounts.resourceGroup, storageAccounts.name);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
if (!ignoreErrors) {
|
||||
throw err;
|
||||
}
|
||||
result.errors.push(err);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
20
extensions/azurecore/src/azurecore.d.ts
vendored
20
extensions/azurecore/src/azurecore.d.ts
vendored
@@ -6,6 +6,8 @@
|
||||
declare module 'azurecore' {
|
||||
import * as azdata from 'azdata';
|
||||
import { azureResource } from 'azureResource';
|
||||
import { BlobContainersListResponse, FileSharesListResponse } from '@azure/arm-storage/esm/models';
|
||||
|
||||
/**
|
||||
* Covers defining what the azurecore extension exports to other extensions
|
||||
*
|
||||
@@ -66,8 +68,14 @@ declare module 'azurecore' {
|
||||
}
|
||||
|
||||
export interface IExtension {
|
||||
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly?: boolean): Thenable<GetSubscriptionsResult>;
|
||||
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Thenable<GetResourceGroupsResult>;
|
||||
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly?: boolean): Promise<GetSubscriptionsResult>;
|
||||
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Promise<GetResourceGroupsResult>;
|
||||
getSqlManagedInstances(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetSqlManagedInstancesResult>;
|
||||
getSqlServers(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetSqlServersResult>;
|
||||
getSqlVMServers(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetSqlVMServersResult>;
|
||||
getStorageAccounts(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetStorageAccountResult>;
|
||||
getBlobContainers(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise<GetBlobContainersResult>;
|
||||
getFileShares(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise<GetFileSharesResult>;
|
||||
/**
|
||||
* Converts a region value (@see AzureRegion) into the localized Display Name
|
||||
* @param region The region value
|
||||
@@ -76,10 +84,18 @@ declare module 'azurecore' {
|
||||
provideResources(): azureResource.IAzureResourceProvider[];
|
||||
|
||||
runGraphQuery<T extends azureResource.AzureGraphResource>(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors: boolean, query: string): Promise<ResourceQueryResult<T>>;
|
||||
makeHttpGetRequest(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, ignoreErrors: boolean, url: string): Promise<HttpGetRequestResult>;
|
||||
}
|
||||
|
||||
export type GetSubscriptionsResult = { subscriptions: azureResource.AzureResourceSubscription[], errors: Error[] };
|
||||
export type GetResourceGroupsResult = { resourceGroups: azureResource.AzureResourceResourceGroup[], errors: Error[] };
|
||||
export type GetSqlManagedInstancesResult = { resources: azureResource.AzureGraphResource[], errors: Error[] };
|
||||
export type GetSqlServersResult = {resources: azureResource.AzureGraphResource[], errors: Error[]};
|
||||
export type GetSqlVMServersResult = {resources: azureResource.AzureGraphResource[], errors: Error[]};
|
||||
export type GetStorageAccountResult = {resources: azureResource.AzureGraphResource[], errors: Error[]};
|
||||
export type GetBlobContainersResult = {blobContainer: BlobContainersListResponse | undefined, errors: Error[]};
|
||||
export type GetFileSharesResult = {fileShares: FileSharesListResponse | undefined, errors: Error[]};
|
||||
|
||||
export type ResourceQueryResult<T extends azureResource.AzureGraphResource> = { resources: T[], errors: Error[] };
|
||||
export type HttpGetRequestResult = { response: any, errors: Error[] };
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import * as vscode from 'vscode';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as resourceDeployment from 'resource-deployment';
|
||||
|
||||
import { AppContext } from './appContext';
|
||||
import { AzureAccountProviderService } from './account-provider/azureAccountProviderService';
|
||||
@@ -86,10 +87,10 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
|
||||
registerAzureServices(appContext);
|
||||
const azureResourceTree = new AzureResourceTreeProvider(appContext);
|
||||
const connectionDialogTree = new ConnectionDialogTreeProvider(appContext);
|
||||
pushDisposable(vscode.window.registerTreeDataProvider('connectionDialog/azureResourceExplorer', connectionDialogTree));
|
||||
pushDisposable(vscode.window.registerTreeDataProvider('azureResourceExplorer', azureResourceTree));
|
||||
pushDisposable(vscode.window.registerTreeDataProvider('connectionDialog/azureResourceExplorer', connectionDialogTree));
|
||||
pushDisposable(vscode.workspace.onDidChangeConfiguration(e => onDidChangeConfiguration(e), this));
|
||||
registerAzureResourceCommands(appContext, [azureResourceTree, connectionDialogTree]);
|
||||
registerAzureResourceCommands(appContext, azureResourceTree, connectionDialogTree);
|
||||
azdata.dataprotocol.registerDataGridProvider(new AzureDataGridProvider(appContext));
|
||||
vscode.commands.registerCommand('azure.dataGrid.openInAzurePortal', async (item: azdata.DataGridItem) => {
|
||||
const portalEndpoint = item.portalEndpoint;
|
||||
@@ -105,13 +106,47 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
|
||||
}
|
||||
});
|
||||
|
||||
// Don't block on this since there's a bit of a circular dependency here with the extension activation since resource deployment
|
||||
// depends on this extension too. It's fine to wait a bit for that to finish before registering the provider
|
||||
vscode.extensions.getExtension(resourceDeployment.extension.name).activate().then((api: resourceDeployment.IExtension) => {
|
||||
context.subscriptions.push(api.registerValueProvider({
|
||||
id: 'subscription-id-to-tenant-id',
|
||||
getValue: async (triggerValue: string) => {
|
||||
if (triggerValue === '') {
|
||||
return '';
|
||||
}
|
||||
let accounts: azdata.Account[] = [];
|
||||
try {
|
||||
accounts = await azdata.accounts.getAllAccounts();
|
||||
} catch (err) {
|
||||
console.warn(`Error fetching accounts for subscription-id-to-tenant-id provider : ${err}`);
|
||||
return '';
|
||||
}
|
||||
|
||||
for (const account of accounts) {
|
||||
// Ignore any errors - they'll be logged in the called function and we still want to look
|
||||
// at any subscriptions that are returned - worst case we'll just return an empty string if we didn't
|
||||
// find the matching subscription
|
||||
const subs = await azureResourceUtils.getSubscriptions(appContext, account, true);
|
||||
const sub = subs.subscriptions.find(sub => sub.id === triggerValue);
|
||||
if (sub) {
|
||||
return sub.tenant;
|
||||
}
|
||||
|
||||
}
|
||||
console.error(`Unable to find subscription with ID ${triggerValue} when mapping subscription ID to tenant ID`);
|
||||
return '';
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
return {
|
||||
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly: boolean = false): Thenable<azurecore.GetSubscriptionsResult> {
|
||||
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly: boolean = false): Promise<azurecore.GetSubscriptionsResult> {
|
||||
return selectedOnly
|
||||
? azureResourceUtils.getSelectedSubscriptions(appContext, account, ignoreErrors)
|
||||
: azureResourceUtils.getSubscriptions(appContext, account, ignoreErrors);
|
||||
},
|
||||
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Thenable<azurecore.GetResourceGroupsResult> { return azureResourceUtils.getResourceGroups(appContext, account, subscription, ignoreErrors); },
|
||||
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Promise<azurecore.GetResourceGroupsResult> { return azureResourceUtils.getResourceGroups(appContext, account, subscription, ignoreErrors); },
|
||||
provideResources(): azureResource.IAzureResourceProvider[] {
|
||||
const arcFeaturedEnabled = vscode.workspace.getConfiguration(constants.extensionConfigSectionName).get('enableArcFeatures');
|
||||
const providers: azureResource.IAzureResourceProvider[] = [
|
||||
@@ -129,12 +164,50 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
|
||||
}
|
||||
return providers;
|
||||
},
|
||||
getSqlManagedInstances(account: azdata.Account,
|
||||
subscriptions: azureResource.AzureResourceSubscription[],
|
||||
ignoreErrors: boolean): Promise<azurecore.GetSqlManagedInstancesResult> {
|
||||
return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, `where type == "${azureResource.AzureResourceType.sqlManagedInstance}"`);
|
||||
},
|
||||
getSqlServers(account: azdata.Account,
|
||||
subscriptions: azureResource.AzureResourceSubscription[],
|
||||
ignoreErrors: boolean): Promise<azurecore.GetSqlServersResult> {
|
||||
return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, `where type == "${azureResource.AzureResourceType.sqlServer}"`);
|
||||
},
|
||||
getSqlVMServers(account: azdata.Account,
|
||||
subscriptions: azureResource.AzureResourceSubscription[],
|
||||
ignoreErrors: boolean): Promise<azurecore.GetSqlVMServersResult> {
|
||||
return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, `where type == "${azureResource.AzureResourceType.virtualMachines}" and properties.storageProfile.imageReference.publisher == "microsoftsqlserver"`);
|
||||
},
|
||||
getStorageAccounts(account: azdata.Account,
|
||||
subscriptions: azureResource.AzureResourceSubscription[],
|
||||
ignoreErrors: boolean): Promise<azurecore.GetStorageAccountResult> {
|
||||
return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, `where type == "${azureResource.AzureResourceType.storageAccount}"`);
|
||||
},
|
||||
getBlobContainers(account: azdata.Account,
|
||||
subscription: azureResource.AzureResourceSubscription,
|
||||
storageAccount: azureResource.AzureGraphResource,
|
||||
ignoreErrors: boolean): Promise<azurecore.GetBlobContainersResult> {
|
||||
return azureResourceUtils.getBlobContainers(account, subscription, storageAccount, ignoreErrors);
|
||||
},
|
||||
getFileShares(account: azdata.Account,
|
||||
subscription: azureResource.AzureResourceSubscription,
|
||||
storageAccount: azureResource.AzureGraphResource,
|
||||
ignoreErrors: boolean): Promise<azurecore.GetFileSharesResult> {
|
||||
return azureResourceUtils.getFileShares(account, subscription, storageAccount, ignoreErrors);
|
||||
},
|
||||
getRegionDisplayName: utils.getRegionDisplayName,
|
||||
runGraphQuery<T extends azureResource.AzureGraphResource>(account: azdata.Account,
|
||||
subscriptions: azureResource.AzureResourceSubscription[],
|
||||
ignoreErrors: boolean,
|
||||
query: string): Promise<azurecore.ResourceQueryResult<T>> {
|
||||
return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, query);
|
||||
},
|
||||
makeHttpGetRequest(account: azdata.Account,
|
||||
subscription: azureResource.AzureResourceSubscription,
|
||||
ignoreErrors: boolean,
|
||||
url: string) {
|
||||
return azureResourceUtils.makeHttpGetRequest(account, subscription, ignoreErrors, url);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -74,3 +74,10 @@ export const azureArcPostgresServer = localize('azurecore.azureArcPostgres', "Az
|
||||
|
||||
export const unableToOpenAzureLink = localize('azure.unableToOpenAzureLink', "Unable to open link, missing required values");
|
||||
export const azureResourcesGridTitle = localize('azure.azureResourcesGridTitle', "Azure Resources (Preview)");
|
||||
|
||||
// Azure Request Errors
|
||||
export const invalidAzureAccount = localize('azurecore.invalidAzureAccount', "Invalid account");
|
||||
export const invalidTenant = localize('azurecore.invalidTenant', "Invalid tenant for subscription");
|
||||
export function unableToFetchTokenError(tenant: string): string {
|
||||
return localize('azurecore.unableToFetchToken', "Unable to get token for tenant {0}", tenant);
|
||||
}
|
||||
|
||||
1
extensions/azurecore/src/typings/ref.d.ts
vendored
1
extensions/azurecore/src/typings/ref.d.ts
vendored
@@ -7,4 +7,5 @@
|
||||
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
|
||||
/// <reference path='../../../../src/sql/azdata.d.ts'/>
|
||||
/// <reference path='../../../../src/sql/azdata.proposed.d.ts'/>
|
||||
/// <reference path='../../../resource-deployment/src/typings/resource-deployment.d.ts'/>
|
||||
/// <reference types='@types/node'/>
|
||||
|
||||
@@ -11,6 +11,15 @@
|
||||
"@azure/ms-rest-js" "^1.8.1"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@azure/arm-storage@^15.1.0":
|
||||
version "15.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@azure/arm-storage/-/arm-storage-15.1.0.tgz#fa14b5e532babf39b47c5cffe89de5aa062e1f80"
|
||||
integrity sha512-IWomHlT7eEnCSMDHH/z5/XyPHhGAIPmWYgHkIyYB2YQt+Af+hWvE1NIwI79Eeiu+Am4U8BKUsXWmWKqDYh0Srg==
|
||||
dependencies:
|
||||
"@azure/ms-rest-azure-js" "^2.0.1"
|
||||
"@azure/ms-rest-js" "^2.0.4"
|
||||
tslib "^1.10.0"
|
||||
|
||||
"@azure/arm-subscriptions@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@azure/arm-subscriptions/-/arm-subscriptions-1.0.0.tgz#ab65a5cd4d8b8c878ff6621428f29137b84eb1d6"
|
||||
@@ -28,6 +37,14 @@
|
||||
"@azure/ms-rest-js" "^1.8.10"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@azure/ms-rest-azure-js@^2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@azure/ms-rest-azure-js/-/ms-rest-azure-js-2.0.1.tgz#fa1b38f039b3ee48a9e086a88c8a5b5b7776491c"
|
||||
integrity sha512-5e+A710O7gRFISoV4KI/ZyLQbKmjXxQZ1L8Z/sx7jSUQqmswjTnN4yyIZxs5JzfLVkobU0rXxbi5/LVzaI8QXQ==
|
||||
dependencies:
|
||||
"@azure/ms-rest-js" "^2.0.4"
|
||||
tslib "^1.10.0"
|
||||
|
||||
"@azure/ms-rest-js@^1.1.0", "@azure/ms-rest-js@^1.8.1", "@azure/ms-rest-js@^1.8.10":
|
||||
version "1.8.14"
|
||||
resolved "https://registry.yarnpkg.com/@azure/ms-rest-js/-/ms-rest-js-1.8.14.tgz#657fc145db20b6eb3d58d1a2055473aa72eb609d"
|
||||
@@ -42,6 +59,22 @@
|
||||
uuid "^3.2.1"
|
||||
xml2js "^0.4.19"
|
||||
|
||||
"@azure/ms-rest-js@^2.0.4":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@azure/ms-rest-js/-/ms-rest-js-2.1.0.tgz#41bc541984983b5242dfbcf699ea281acd045946"
|
||||
integrity sha512-4BXLVImYRt+jcUmEJ5LUWglI8RBNVQndY6IcyvQ4U8O4kIXdmlRz3cJdA/RpXf5rKT38KOoTO2T6Z1f6Z1HDBg==
|
||||
dependencies:
|
||||
"@types/node-fetch" "^2.3.7"
|
||||
"@types/tunnel" "0.0.1"
|
||||
abort-controller "^3.0.0"
|
||||
form-data "^2.5.0"
|
||||
node-fetch "^2.6.0"
|
||||
tough-cookie "^3.0.1"
|
||||
tslib "^1.10.0"
|
||||
tunnel "0.0.6"
|
||||
uuid "^3.3.2"
|
||||
xml2js "^0.4.19"
|
||||
|
||||
"@babel/code-frame@^7.8.3":
|
||||
version "7.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e"
|
||||
@@ -275,6 +308,14 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.7.tgz#315d570ccb56c53452ff8638738df60726d5b6ea"
|
||||
integrity sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==
|
||||
|
||||
"@types/node-fetch@^2.3.7":
|
||||
version "2.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c"
|
||||
integrity sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
form-data "^3.0.0"
|
||||
|
||||
"@types/node@*":
|
||||
version "13.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.9.5.tgz#59738bf30b31aea1faa2df7f4a5f55613750cf00"
|
||||
@@ -324,6 +365,13 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/tunnel@0.0.1":
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/tunnel/-/tunnel-0.0.1.tgz#0d72774768b73df26f25df9184273a42da72b19c"
|
||||
integrity sha512-AOqu6bQu5MSWwYvehMXLukFHnupHrpZ8nvgae5Ggie9UwzDR1CCwoXgSSWNZJuyOlCdfdsWMA5F2LlmvyoTv8A==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/ws@^6.0.4":
|
||||
version "6.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-6.0.4.tgz#7797707c8acce8f76d8c34b370d4645b70421ff1"
|
||||
@@ -331,6 +379,13 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
abort-controller@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
|
||||
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
|
||||
dependencies:
|
||||
event-target-shim "^5.0.0"
|
||||
|
||||
ansi-regex@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
|
||||
@@ -466,7 +521,7 @@ color-name@1.1.3:
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
||||
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
|
||||
|
||||
combined-stream@^1.0.6:
|
||||
combined-stream@^1.0.6, combined-stream@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||
@@ -596,6 +651,11 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5:
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
|
||||
|
||||
event-target-shim@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
|
||||
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
|
||||
|
||||
expand-template@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
|
||||
@@ -617,6 +677,15 @@ form-data@^2.3.2, form-data@^2.5.0:
|
||||
combined-stream "^1.0.6"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
form-data@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682"
|
||||
integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
fs-constants@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
|
||||
@@ -733,6 +802,11 @@ ini@~1.3.0:
|
||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
||||
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
|
||||
|
||||
ip-regex@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
|
||||
integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=
|
||||
|
||||
is-buffer@~1.1.1:
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
|
||||
@@ -1009,6 +1083,11 @@ node-abi@^2.7.0:
|
||||
dependencies:
|
||||
semver "^5.4.1"
|
||||
|
||||
node-fetch@^2.6.0:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
|
||||
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
|
||||
|
||||
noop-logger@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2"
|
||||
@@ -1389,6 +1468,20 @@ tough-cookie@^2.4.3:
|
||||
psl "^1.1.28"
|
||||
punycode "^2.1.1"
|
||||
|
||||
tough-cookie@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2"
|
||||
integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==
|
||||
dependencies:
|
||||
ip-regex "^2.1.0"
|
||||
psl "^1.1.28"
|
||||
punycode "^2.1.1"
|
||||
|
||||
tslib@^1.10.0:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^1.9.2, tslib@^1.9.3:
|
||||
version "1.11.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
|
||||
@@ -1425,7 +1518,7 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
||||
|
||||
uuid@^3.2.1:
|
||||
uuid@^3.2.1, uuid@^3.3.2:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
||||
|
||||
2
extensions/azurehybridtoolkit/.gitignore
vendored
Normal file
2
extensions/azurehybridtoolkit/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
notebooks/hybridbook/Components/**/obj
|
||||
*.vsix
|
||||
@@ -1,4 +1,7 @@
|
||||
.gitignore
|
||||
src/**
|
||||
out/**
|
||||
tsconfig.json
|
||||
extension.webpack.config.js
|
||||
*.vsix
|
||||
yarn.lock
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Azure SQL Hybrid Cloud Toolkit Jupyter Book Extension for Azure Data Studio
|
||||
# Azure SQL Hybrid Cloud Toolkit *(preview)*
|
||||
|
||||
Welcome to the Azure SQL Hybrid Cloud Toolkit Jupyter Book Extension for Azure Data Studio! This extension opens a Jupyter Book that has several utilities for Azure SQL such as migration assessments and setting up networking connectivity.
|
||||
Adds a Jupyter Book that has several utilities for Azure SQL Hybrid Cloud.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
|
||||
BIN
extensions/azurehybridtoolkit/images/extension.png
Normal file
BIN
extensions/azurehybridtoolkit/images/extension.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
@@ -8,6 +8,15 @@
|
||||
- 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
|
||||
@@ -19,15 +28,6 @@
|
||||
url: networking/p2svnet-creation
|
||||
- title: Create Site-to-Site VPN
|
||||
url: networking/s2svnet-creation
|
||||
- title: Assessments
|
||||
url: /Assessments/readme
|
||||
not_numbered: true
|
||||
expand_sections: false
|
||||
sections:
|
||||
- title: SQL Server Best Practices Assessment
|
||||
url: Assessments/sql-server-assessment
|
||||
- title: Compatibility Assessment
|
||||
url: Assessments/compatibility-assessment
|
||||
- title: Provisioning
|
||||
url: /provisioning/readme
|
||||
not_numbered: true
|
||||
@@ -57,8 +57,6 @@
|
||||
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
|
||||
@@ -68,13 +66,9 @@
|
||||
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
|
||||
url: /appendices
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
---
|
||||
|
||||
@import 'main';
|
||||
@@ -1,4 +0,0 @@
|
||||
/* Put your custom CSS here */
|
||||
.left {
|
||||
margin-left: 0px;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
// Put your custom javascript here
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
permalink: /index.html
|
||||
title: "Index"
|
||||
layout: none
|
||||
---
|
||||
|
||||
<!-- The index page should simply re-direct to the first chapter -->
|
||||
{% 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 %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-US">
|
||||
<meta charset="utf-8">
|
||||
<title>Redirecting…</title>
|
||||
<link rel="canonical" href="{{ redirectURL }}">
|
||||
<script>location="{{ redirectURL }}"</script>
|
||||
<meta http-equiv="refresh" content="0; url={{ redirectURL }}">
|
||||
<meta name="robots" content="noindex">
|
||||
<h1>Redirecting…</h1>
|
||||
<a href="{{ redirectURL }}">Click here if you are not redirected.</a>
|
||||
</html>
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
permalink: /search
|
||||
title: "Search the site"
|
||||
search_page: true
|
||||
---
|
||||
|
||||
<div class="search-content__inner-wrap">
|
||||
<input type="text" id="lunr_search" class="search-input" tabindex="-1" placeholder="'Enter your search term...''" />
|
||||
<div id="results" class="results"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Add the lunr store since we will now search it
|
||||
{% include search/lunr/lunr-store.js %}
|
||||
</script>
|
||||
@@ -1 +0,0 @@
|
||||
<svg aria-hidden="true" data-prefix="far" data-icon="copy" class="svg-inline--fa fa-copy fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="#777" d="M433.941 65.941l-51.882-51.882A48 48 0 0 0 348.118 0H176c-26.51 0-48 21.49-48 48v48H48c-26.51 0-48 21.49-48 48v320c0 26.51 21.49 48 48 48h224c26.51 0 48-21.49 48-48v-48h80c26.51 0 48-21.49 48-48V99.882a48 48 0 0 0-14.059-33.941zM266 464H54a6 6 0 0 1-6-6V150a6 6 0 0 1 6-6h74v224c0 26.51 21.49 48 48 48h96v42a6 6 0 0 1-6 6zm128-96H182a6 6 0 0 1-6-6V54a6 6 0 0 1 6-6h106v88c0 13.255 10.745 24 24 24h88v202a6 6 0 0 1-6 6zm6-256h-64V48h9.632c1.591 0 3.117.632 4.243 1.757l48.368 48.368a6 6 0 0 1 1.757 4.243V112z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 711 B |
@@ -1,81 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
id="svg2157"
|
||||
height="600"
|
||||
width="600"
|
||||
version="1.0"
|
||||
sodipodi:docname="edit-button.svg"
|
||||
inkscape:version="0.92.3 (2405546, 2018-03-11)">
|
||||
<defs
|
||||
id="defs12" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1001"
|
||||
id="namedview10"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.39333333"
|
||||
inkscape:cx="300"
|
||||
inkscape:cy="300"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<metadata
|
||||
id="metadata2162">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
id="layer1"
|
||||
style="fill:#ffffff">
|
||||
<g
|
||||
id="g3765"
|
||||
stroke="#a2a9b1"
|
||||
fill="none"
|
||||
style="fill:none;stroke:#6c7681;stroke-opacity:1;stroke-linecap:round;paint-order:normal;stroke-linejoin:miter;stroke-width:40;stroke-miterlimit:5;stroke-dasharray:none">
|
||||
<path
|
||||
id="rect2990"
|
||||
d="m70.064 422.35 374.27-374.26 107.58 107.58-374.26 374.27-129.56 21.97z"
|
||||
stroke-width="30"
|
||||
style="fill:none;stroke:#6c7681;stroke-opacity:1;stroke-linecap:round;paint-order:normal;stroke-linejoin:miter;stroke-width:40;stroke-miterlimit:5;stroke-dasharray:none" />
|
||||
<path
|
||||
id="path3771"
|
||||
d="m70.569 417.81 110.61 110.61"
|
||||
stroke-width="25"
|
||||
style="fill:none;stroke:#6c7681;stroke-opacity:1;stroke-linecap:round;paint-order:normal;stroke-linejoin:miter;stroke-width:40;stroke-miterlimit:5;stroke-dasharray:none" />
|
||||
<path
|
||||
id="path3777"
|
||||
d="m491.47 108.37-366.69 366.68"
|
||||
stroke-width="25"
|
||||
style="fill:none;stroke:#6c7681;stroke-opacity:1;stroke-linecap:round;paint-order:normal;stroke-linejoin:miter;stroke-width:40;stroke-miterlimit:5;stroke-dasharray:none" />
|
||||
<path
|
||||
id="path3763"
|
||||
d="m54.222 507.26 40.975 39.546"
|
||||
stroke-width="25"
|
||||
style="fill:none;stroke:#6c7681;stroke-opacity:1;stroke-linecap:round;paint-order:normal;stroke-linejoin:miter;stroke-width:40;stroke-miterlimit:5;stroke-dasharray:none" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.9 KiB |
@@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 44.4 44.4" style="enable-background:new 0 0 44.4 44.4;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:none;stroke:#F5A252;stroke-width:5;stroke-miterlimit:10;}
|
||||
.st1{fill:none;stroke:#579ACA;stroke-width:5;stroke-miterlimit:10;}
|
||||
.st2{fill:none;stroke:#E66581;stroke-width:5;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<title>logo</title>
|
||||
<g>
|
||||
<path class="st0" d="M33.9,6.4c3.6,3.9,3.4,9.9-0.5,13.5s-9.9,3.4-13.5-0.5s-3.4-9.9,0.5-13.5l0,0C24.2,2.4,30.2,2.6,33.9,6.4z"/>
|
||||
<path class="st1" d="M35.1,27.3c2.6,4.6,1.1,10.4-3.5,13c-4.6,2.6-10.4,1.1-13-3.5s-1.1-10.4,3.5-13l0,0
|
||||
C26.6,21.2,32.4,22.7,35.1,27.3z"/>
|
||||
<path class="st2" d="M25.9,17.8c2.6,4.6,1.1,10.4-3.5,13s-10.4,1.1-13-3.5s-1.1-10.4,3.5-13l0,0C17.5,11.7,23.3,13.2,25.9,17.8z"/>
|
||||
<path class="st1" d="M19.2,26.4c3.1-4.3,9.1-5.2,13.3-2.1c1.1,0.8,2,1.8,2.7,3"/>
|
||||
<path class="st0" d="M19.9,19.4c-3.6-3.9-3.4-9.9,0.5-13.5s9.9-3.4,13.5,0.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1 +0,0 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="38.73" height="50" viewBox="0 0 38.73 50"><defs><style>.cls-1{fill:#767677;}.cls-2{fill:#f37726;}.cls-3{fill:#9e9e9e;}.cls-4{fill:#616262;}.cls-5{font-size:17.07px;fill:#fff;font-family:Roboto-Regular, Roboto;}</style></defs><title>logo_jupyterhub</title><g id="Canvas"><path id="path7_fill" data-name="path7 fill" class="cls-1" d="M39.51,3.53a3,3,0,0,1-1.7,2.9A3,3,0,0,1,34.48,6a3,3,0,0,1-.82-3.26,3,3,0,0,1,1.05-1.41A3,3,0,0,1,37.52.86a2.88,2.88,0,0,1,1,.6,3,3,0,0,1,.7.93,3.18,3.18,0,0,1,.28,1.14Z" transform="translate(-1.87 -0.69)"/><path id="path8_fill" data-name="path8 fill" class="cls-2" d="M21.91,38.39c-8,0-15.06-2.87-18.7-7.12a19.93,19.93,0,0,0,37.39,0C37,35.52,30,38.39,21.91,38.39Z" transform="translate(-1.87 -0.69)"/><path id="path9_fill" data-name="path9 fill" class="cls-2" d="M21.91,10.78c8,0,15.05,2.87,18.69,7.12a19.93,19.93,0,0,0-37.39,0C6.85,13.64,13.86,10.78,21.91,10.78Z" transform="translate(-1.87 -0.69)"/><path id="path10_fill" data-name="path10 fill" class="cls-3" d="M10.88,46.66a3.86,3.86,0,0,1-.52,2.15,3.81,3.81,0,0,1-1.62,1.51,3.93,3.93,0,0,1-2.19.34,3.79,3.79,0,0,1-2-.94,3.73,3.73,0,0,1-1.14-1.9,3.79,3.79,0,0,1,.1-2.21,3.86,3.86,0,0,1,1.33-1.78,3.92,3.92,0,0,1,3.54-.53,3.85,3.85,0,0,1,2.14,1.93,3.74,3.74,0,0,1,.37,1.43Z" transform="translate(-1.87 -0.69)"/><path id="path11_fill" data-name="path11 fill" class="cls-4" d="M4.12,9.81A2.18,2.18,0,0,1,2.9,9.48a2.23,2.23,0,0,1-.84-1A2.26,2.26,0,0,1,1.9,7.26a2.13,2.13,0,0,1,.56-1.13,2.18,2.18,0,0,1,2.36-.56,2.13,2.13,0,0,1,1,.76,2.18,2.18,0,0,1,.42,1.2A2.22,2.22,0,0,1,4.12,9.81Z" transform="translate(-1.87 -0.69)"/></g><text class="cls-5" transform="translate(5.24 30.01)">Hub</text></svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.5 KiB |
File diff suppressed because one or more lines are too long
@@ -1,58 +0,0 @@
|
||||
(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<this.w.length;b++)this.w[b]&&(a[Math.floor(b/6)]^=1<<b%6);for(b=0;b<a.length;b++)a[b]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".charAt(a[b]||0);return a.join("")+"~"};var vd=new $c;function J(a){vd.set(a)}var Td=function(a){a=Dd(a);a=new $c(a);for(var b=vd.w.slice(),c=0;c<a.w.length;c++)b[c]=b[c]||a.w[c];return(new $c(b)).encode()},Dd=function(a){a=a.get(Gd);ka(a)||(a=[]);return a};var ea=function(a){return"function"==typeof a},ka=function(a){return"[object Array]"==Object.prototype.toString.call(Object(a))},qa=function(a){return void 0!=a&&-1<(a.constructor+"").indexOf("String")},D=function(a,b){return 0==a.indexOf(b)},sa=function(a){return a?a.replace(/^[\s\xa0]+|[\s\xa0]+$/g,""):""},ra=function(){for(var a=O.navigator.userAgent+(M.cookie?M.cookie:"")+(M.referrer?M.referrer:""),b=a.length,c=O.history.length;0<c;)a+=c--^b++;return[hd()^La(a)&2147483647,Math.round((new Date).getTime()/
|
||||
1E3)].join(".")},ta=function(a){var b=M.createElement("img");b.width=1;b.height=1;b.src=a;return b},ua=function(){},K=function(a){if(encodeURIComponent instanceof Function)return encodeURIComponent(a);J(28);return a},L=function(a,b,c,d){try{a.addEventListener?a.addEventListener(b,c,!!d):a.attachEvent&&a.attachEvent("on"+b,c)}catch(e){J(27)}},f=/^[\w\-:/.?=&%!]+$/,wa=function(a,b,c){a&&(c?(c="",b&&f.test(b)&&(c=' id="'+b+'"'),f.test(a)&&M.write("<script"+c+' src="'+a+'">\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<d;e++)if("object"===typeof b[e]){for(var g in b[e])b[e].hasOwnProperty(g)&&(c[g]=b[e][g]);break}else e<a.length&&(c[a[e]]=b[e]);return c};var ee=function(){this.keys=[];this.values={};this.m={}};ee.prototype.set=function(a,b,c){this.keys.push(a);c?this.m[":"+a]=b:this.values[":"+a]=b};ee.prototype.get=function(a){return this.m.hasOwnProperty(":"+a)?this.m[":"+a]:this.values[":"+a]};ee.prototype.map=function(a){for(var b=0;b<this.keys.length;b++){var c=this.keys[b],d=this.get(c);d&&a(c,d)}};var O=window,M=document,va=function(a,b){return setTimeout(a,b)};var F=window,Ea=document,G=function(a){var b=F._gaUserPrefs;if(b&&b.ioo&&b.ioo()||a&&!0===F["ga-disable-"+a])return!0;try{var c=F.external;if(c&&c._gaUserPrefs&&"oo"==c._gaUserPrefs)return!0}catch(g){}a=[];b=Ea.cookie.split(";");c=/^\s*AMP_TOKEN=\s*(.*?)\s*$/;for(var d=0;d<b.length;d++){var e=b[d].match(c);e&&a.push(e[1])}for(b=0;b<a.length;b++)if("$OPT_OUT"==decodeURIComponent(a[b]))return!0;return!1};var Ca=function(a){var b=[],c=M.cookie.split(";");a=new RegExp("^\\s*"+a+"=\\s*(.*?)\\s*$");for(var d=0;d<c.length;d++){var e=c[d].match(a);e&&b.push(e[1])}return b},zc=function(a,b,c,d,e,g){e=G(e)?!1:eb.test(M.location.hostname)||"/"==c&&vc.test(d)?!1:!0;if(!e)return!1;b&&1200<b.length&&(b=b.substring(0,1200));c=a+"="+b+"; path="+c+"; ";g&&(c+="expires="+(new Date((new Date).getTime()+g)).toGMTString()+"; ");d&&"none"!==d&&(c+="domain="+d+";");d=M.cookie;M.cookie=c;if(!(d=d!=M.cookie))a:{a=Ca(a);
|
||||
for(d=0;d<a.length;d++)if(b==a[d]){d=!0;break a}d=!1}return d},Cc=function(a){return encodeURIComponent?encodeURIComponent(a).replace(/\(/g,"%28").replace(/\)/g,"%29"):a},vc=/^(www\.)?google(\.com?)?(\.[a-z]{2})?$/,eb=/(^|\.)doubleclick\.net$/i;var oc,Id=/^.*Version\/?(\d+)[^\d].*$/i,ne=function(){if(void 0!==O.__ga4__)return O.__ga4__;if(void 0===oc){var a=O.navigator.userAgent;if(a){var b=a;try{b=decodeURIComponent(a)}catch(c){}if(a=!(0<=b.indexOf("Chrome"))&&!(0<=b.indexOf("CriOS"))&&(0<=b.indexOf("Safari/")||0<=b.indexOf("Safari,")))b=Id.exec(b),a=11<=(b?Number(b[1]):-1);oc=a}else oc=!1}return oc};var Fa,Ga,fb,Ab,ja=/^https?:\/\/[^/]*cdn\.ampproject\.org\//,Ub=[],ic=function(){Z.D([ua])},tc=function(a,b){var c=Ca("AMP_TOKEN");if(1<c.length)return J(55),!1;c=decodeURIComponent(c[0]||"");if("$OPT_OUT"==c||"$ERROR"==c||G(b))return J(62),!1;if(!ja.test(M.referrer)&&"$NOT_FOUND"==c)return J(68),!1;if(void 0!==Ab)return J(56),va(function(){a(Ab)},0),!0;if(Fa)return Ub.push(a),!0;if("$RETRIEVING"==c)return J(57),va(function(){tc(a,b)},1E4),!0;Fa=!0;c&&"$"!=c[0]||(xc("$RETRIEVING",3E4),setTimeout(Mc,
|
||||
3E4),c="");return Pc(c,b)?(Ub.push(a),!0):!1},Pc=function(a,b,c){if(!window.JSON)return J(58),!1;var d=O.XMLHttpRequest;if(!d)return J(59),!1;var e=new d;if(!("withCredentials"in e))return J(60),!1;e.open("POST",(c||"https://ampcid.google.com/v1/publisher:getClientId")+"?key=AIzaSyA65lEHUEizIsNtlbNo-l2K18dT680nsaM",!0);e.withCredentials=!0;e.setRequestHeader("Content-Type","text/plain");e.onload=function(){Fa=!1;if(4==e.readyState){try{200!=e.status&&(J(61),Qc("","$ERROR",3E4));var d=JSON.parse(e.responseText);
|
||||
d.optOut?(J(63),Qc("","$OPT_OUT",31536E6)):d.clientId?Qc(d.clientId,d.securityToken,31536E6):!c&&d.alternateUrl?(Ga&&clearTimeout(Ga),Fa=!0,Pc(a,b,d.alternateUrl)):(J(64),Qc("","$NOT_FOUND",36E5))}catch(ca){J(65),Qc("","$ERROR",3E4)}e=null}};d={originScope:"AMP_ECID_GOOGLE"};a&&(d.securityToken=a);e.send(JSON.stringify(d));Ga=va(function(){J(66);Qc("","$ERROR",3E4)},1E4);return!0},Mc=function(){Fa=!1},xc=function(a,b){if(void 0===fb){fb="";for(var c=id(),d=0;d<c.length;d++){var e=c[d];if(zc("AMP_TOKEN",
|
||||
encodeURIComponent(a),"/",e,"",b)){fb=e;return}}}zc("AMP_TOKEN",encodeURIComponent(a),"/",fb,"",b)},Qc=function(a,b,c){Ga&&clearTimeout(Ga);b&&xc(b,c);Ab=a;b=Ub;Ub=[];for(c=0;c<b.length;c++)b[c](a)};var oe=function(){return(Ba||"https:"==M.location.protocol?"https:":"http:")+"//www.google-analytics.com"},Da=function(a){this.name="len";this.message=a+"-8192"},ba=function(a,b,c){c=c||ua;if(2036>=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<d.count++)ge("xhr","tmr",""+d.count),c();else if(1==a.length)c();else{var b=a.charAt(1);if("d"==b)pe("https://stats.g.doubleclick.net/j/collect",
|
||||
d.U,d,c);else if("g"==b){var e="https://www.google.%/ads/ga-audiences".replace("%","com");wc(e,d.google,c);var w=a.substring(2);if(w)if(/^[a-z.]{1,6}$/.test(w)){var ha="https://www.google.%/ads/ga-audiences".replace("%",w);wc(ha,d.google,ua)}else ge("tld","bcc",w)}else ge("xhr","brc",b),c()}}catch(ue){ge("xhr","rsp"),c()}else c();g=null}};g.send(b);return!0},x=function(a,b,c){return O.navigator.sendBeacon?O.navigator.sendBeacon(a,b)?(c(),!0):!1:!1},ge=function(a,b,c){1<=100*Math.random()||G("?")||
|
||||
(a=["t=error","_e="+a,"_v=j68","sr=1"],b&&a.push("_f="+b),c&&a.push("_m="+K(c.substring(0,100))),a.push("aip=1"),a.push("z="+hd()),wc("https://www.google-analytics.com/u/d",a.join("&"),ua))};var h=function(a){var b=O.gaData=O.gaData||{};return b[a]=b[a]||{}};var Ha=function(){this.M=[]};Ha.prototype.add=function(a){this.M.push(a)};Ha.prototype.D=function(a){try{for(var b=0;b<this.M.length;b++){var c=a.get(this.M[b]);c&&ea(c)&&c.call(O,a)}}catch(d){}b=a.get(Ia);b!=ua&&ea(b)&&(a.set(Ia,ua,!0),setTimeout(b,10))};function Ja(a){if(100!=a.get(Ka)&&La(P(a,Q))%1E4>=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<e&&(c=Math.min(c+e,20),a.set(Xa,d));if(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<Za.length;c++){var d=Za[c],e=d[0].exec(a);if(e){b=d[1](e);Qa.set(b.name,b);break}}return b},yc=function(a){var b;Qa.map(function(c,d){d.F==a&&(b=d)});return b&&b.name},S=function(a,b,c,d,e){a=new bb(a,b,c,d,e);Qa.set(a.name,a);return a.name},cb=function(a,
|
||||
b){Za.push([new RegExp("^"+a+"$"),b])},T=function(a,b,c){return S(a,b,c,void 0,db)},db=function(){};var gb=qa(window.GoogleAnalyticsObject)&&sa(window.GoogleAnalyticsObject)||"ga",jd=/^(?:utma\.)?\d+\.\d+$/,kd=/^amp-[\w.-]{22,64}$/,Ba=!1,hb=T("apiVersion","v"),ib=T("clientVersion","_v");S("anonymizeIp","aip");var jb=S("adSenseId","a"),Va=S("hitType","t"),Ia=S("hitCallback"),Ra=S("hitPayload");S("nonInteraction","ni");S("currencyCode","cu");S("dataSource","ds");var Vd=S("useBeacon",void 0,!1),fa=S("transport");S("sessionControl","sc","");S("sessionGroup","sg");S("queueTime","qt");var Ac=S("_s","_s");
|
||||
S("screenName","cd");var kb=S("location","dl",""),lb=S("referrer","dr"),mb=S("page","dp","");S("hostname","dh");var nb=S("language","ul"),ob=S("encoding","de");S("title","dt",function(){return M.title||void 0});cb("contentGroup([0-9]+)",function(a){return new bb(a[0],"cg"+a[1])});var pb=S("screenColors","sd"),qb=S("screenResolution","sr"),rb=S("viewportSize","vp"),sb=S("javaEnabled","je"),tb=S("flashVersion","fl");S("campaignId","ci");S("campaignName","cn");S("campaignSource","cs");
|
||||
S("campaignMedium","cm");S("campaignKeyword","ck");S("campaignContent","cc");var ub=S("eventCategory","ec"),xb=S("eventAction","ea"),yb=S("eventLabel","el"),zb=S("eventValue","ev"),Bb=S("socialNetwork","sn"),Cb=S("socialAction","sa"),Db=S("socialTarget","st"),Eb=S("l1","plt"),Fb=S("l2","pdt"),Gb=S("l3","dns"),Hb=S("l4","rrt"),Ib=S("l5","srt"),Jb=S("l6","tcp"),Kb=S("l7","dit"),Lb=S("l8","clt"),Mb=S("timingCategory","utc"),Nb=S("timingVar","utv"),Ob=S("timingLabel","utl"),Pb=S("timingValue","utt");
|
||||
S("appName","an");S("appVersion","av","");S("appId","aid","");S("appInstallerId","aiid","");S("exDescription","exd");S("exFatal","exf");var Nc=S("expId","xid"),Oc=S("expVar","xvar"),m=S("exp","exp"),Rc=S("_utma","_utma"),Sc=S("_utmz","_utmz"),Tc=S("_utmht","_utmht"),Ua=S("_hc",void 0,0),Xa=S("_ti",void 0,0),Wa=S("_to",void 0,20);cb("dimension([0-9]+)",function(a){return new bb(a[0],"cd"+a[1])});cb("metric([0-9]+)",function(a){return new bb(a[0],"cm"+a[1])});S("linkerParam",void 0,void 0,Bc,db);
|
||||
var ld=S("usage","_u"),Gd=S("_um");S("forceSSL",void 0,void 0,function(){return Ba},function(a,b,c){J(34);Ba=!!c});var ed=S("_j1","jid"),ia=S("_j2","gjid");cb("\\&(.*)",function(a){var b=new bb(a[0],a[1]),c=yc(a[0].substring(1));c&&(b.Z=function(a){return a.get(c)},b.o=function(a,b,g,ca){a.set(c,g,ca)},b.F=void 0);return b});
|
||||
var Qb=T("_oot"),dd=S("previewTask"),Rb=S("checkProtocolTask"),md=S("validationTask"),Sb=S("checkStorageTask"),Uc=S("historyImportTask"),Tb=S("samplerTask"),Vb=S("_rlt"),Wb=S("buildHitTask"),Xb=S("sendHitTask"),Vc=S("ceTask"),zd=S("devIdTask"),Cd=S("timingTask"),Ld=S("displayFeaturesTask"),oa=S("customTask"),V=T("name"),Q=T("clientId","cid"),n=T("clientIdTime"),xd=T("storedClientId"),Ad=S("userId","uid"),Na=T("trackingId","tid"),U=T("cookieName",void 0,"_ga"),W=T("cookieDomain"),Yb=T("cookiePath",
|
||||
void 0,"/"),Zb=T("cookieExpires",void 0,63072E3),Hd=T("cookieUpdate",void 0,!0),$b=T("legacyCookieDomain"),Wc=T("legacyHistoryImport",void 0,!0),ac=T("storage",void 0,"cookie"),bc=T("allowLinker",void 0,!1),cc=T("allowAnchor",void 0,!0),Ka=T("sampleRate","sf",100),dc=T("siteSpeedSampleRate",void 0,1),ec=T("alwaysSendReferrer",void 0,!1),I=T("_gid","_gid"),la=T("_gcn"),Kd=T("useAmpClientId"),ce=T("_gclid"),fe=T("_gt"),he=T("_ge",void 0,7776E6),ie=T("_gclsrc"),je=T("storeGac",void 0,!0),gd=S("transportUrl"),
|
||||
Md=S("_r","_r"),qe=S("_dp"),Ud=S("allowAdFeatures",void 0,!0);function X(a,b,c,d){b[a]=function(){try{return d&&J(d),c.apply(this,arguments)}catch(e){throw ge("exc",a,e&&e.name),e;}}};var Od=function(){this.V=100;this.$=this.fa=!1;this.oa="detourexp";this.groups=1},Ed=function(a){var b=new Od,c;if(b.fa&&b.$)return 0;b.$=!0;if(a){if(b.oa&&void 0!==a.get(b.oa))return R(a,b.oa);if(0==a.get(dc))return 0}if(0==b.V)return 0;void 0===c&&(c=Bd());return 0==c%b.V?Math.floor(c/b.V)%b.groups+1:0};function fc(){var a,b;if((b=(b=O.navigator)?b.plugins:null)&&b.length)for(var c=0;c<b.length&&!a;c++){var d=b[c];-1<d.name.indexOf("Shockwave Flash")&&(a=d.description)}if(!a)try{var e=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");a=e.GetVariable("$version")}catch(g){}if(!a)try{e=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6"),a="WIN 6,0,21,0",e.AllowScriptAccess="always",a=e.GetVariable("$version")}catch(g){}if(!a)try{e=new ActiveXObject("ShockwaveFlash.ShockwaveFlash"),a=e.GetVariable("$version")}catch(g){}a&&
|
||||
(e=a.match(/[\d]+/g))&&3<=e.length&&(a=e[0]+"."+e[1]+" r"+e[2]);return a||void 0};var aa=function(a){var b=Math.min(R(a,dc),100);return La(P(a,Q))%100>=b?!1:!0},gc=function(a){var b={};if(Ec(b)||Fc(b)){var c=b[Eb];void 0==c||Infinity==c||isNaN(c)||(0<c?(Y(b,Gb),Y(b,Jb),Y(b,Ib),Y(b,Fb),Y(b,Hb),Y(b,Kb),Y(b,Lb),va(function(){a(b)},10)):L(O,"load",function(){gc(a)},!1))}},Ec=function(a){var b=O.performance||O.webkitPerformance;b=b&&b.timing;if(!b)return!1;var c=b.navigationStart;if(0==c)return!1;a[Eb]=b.loadEventStart-c;a[Gb]=b.domainLookupEnd-b.domainLookupStart;a[Jb]=b.connectEnd-
|
||||
b.connectStart;a[Ib]=b.responseStart-b.requestStart;a[Fb]=b.responseEnd-b.responseStart;a[Hb]=b.fetchStart-c;a[Kb]=b.domInteractive-c;a[Lb]=b.domContentLoadedEventStart-c;return!0},Fc=function(a){if(O.top!=O)return!1;var b=O.external,c=b&&b.onloadT;b&&!b.isValidLoadTime&&(c=void 0);2147483648<c&&(c=void 0);0<c&&b.setPageReadyTime();if(void 0==c)return!1;a[Eb]=c;return!0},Y=function(a,b){var c=a[b];if(isNaN(c)||Infinity==c||0>c)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=0<E(b.get(kb),"gclid").length;(c||d)&&gc(function(b){c&&a.send("timing",b);d&&a.send("adtiming",b)})}}};var hc=!1,mc=function(a){if("cookie"==P(a,ac)){if(a.get(Hd)||P(a,xd)!=P(a,Q)){var b=1E3*R(a,Zb);ma(a,Q,U,b)}ma(a,I,la,864E5);if(a.get(je)){var c=a.get(ce);if(c){var d=Math.min(R(a,he),1E3*R(a,Zb));d=Math.min(d,1E3*R(a,fe)+d-(new Date).getTime());a.data.set(he,d);b={};var e=a.get(fe),g=a.get(ie),ca=kc(P(a,Yb)),l=lc(P(a,W)),k=P(a,Na);g&&"aw.ds"!=g?b&&(b.ua=!0):(c=["1",e,Cc(c)].join("."),0<d&&(b&&(b.ta=!0),zc("_gac_"+Cc(k),c,ca,l,k,d)));le(b)}}else J(75);if(a="none"===lc(P(a,W)))a=M.location.hostname,
|
||||
a=eb.test(a)||vc.test(a);a&&J(30)}},ma=function(a,b,c,d){var e=nd(a,b);if(e){c=P(a,c);var g=kc(P(a,Yb)),ca=lc(P(a,W)),l=P(a,Na);if("auto"!=ca)zc(c,e,g,ca,l,d)&&(hc=!0);else{J(32);for(var k=id(),w=0;w<k.length;w++)if(ca=k[w],a.data.set(W,ca),e=nd(a,b),zc(c,e,g,ca,l,d)){hc=!0;return}a.data.set(W,"auto")}}},nc=function(a){if("cookie"==P(a,ac)&&!hc&&(mc(a),!hc))throw"abort";},Yc=function(a){if(a.get(Wc)){var b=P(a,W),c=P(a,$b)||xa(),d=Xc("__utma",c,b);d&&(J(19),a.set(Tc,(new Date).getTime(),!0),a.set(Rc,
|
||||
d.R),(b=Xc("__utmz",c,b))&&d.hash==b.hash&&a.set(Sc,b.R))}},nd=function(a,b){b=Cc(P(a,b));var c=lc(P(a,W)).split(".").length;a=jc(P(a,Yb));1<a&&(c+="-"+a);return b?["GA1",c,b].join("."):""},Xd=function(a,b){return na(b,P(a,W),P(a,Yb))},na=function(a,b,c){if(!a||1>a.length)J(12);else{for(var d=[],e=0;e<a.length;e++){var g=a[e];var ca=g.split(".");var l=ca.shift();("GA1"==l||"1"==l)&&1<ca.length?(g=ca.shift().split("-"),1==g.length&&(g[1]="1"),g[0]*=1,g[1]*=1,ca={H:g,s:ca.join(".")}):ca=kd.test(g)?
|
||||
{H:[0,0],s:g}:void 0;ca&&d.push(ca)}if(1==d.length)return J(13),d[0].s;if(0==d.length)J(12);else{J(14);d=Gc(d,lc(b).split(".").length,0);if(1==d.length)return d[0].s;d=Gc(d,jc(c),1);1<d.length&&J(41);return d[0]&&d[0].s}}},Gc=function(a,b,c){for(var d=[],e=[],g,ca=0;ca<a.length;ca++){var l=a[ca];l.H[c]==b?d.push(l):void 0==g||l.H[c]<g?(e=[l],g=l.H[c]):l.H[c]==g&&e.push(l)}return 0<d.length?d:e},lc=function(a){return 0==a.indexOf(".")?a.substr(1):a},id=function(){var a=[],b=xa().split(".");if(4==b.length){var c=
|
||||
b[b.length-1];if(parseInt(c,10)==c)return["none"]}for(c=b.length-2;0<=c;c--)a.push(b.slice(c).join("."));a.push("none");return a},kc=function(a){if(!a)return"/";1<a.length&&a.lastIndexOf("/")==a.length-1&&(a=a.substr(0,a.length-1));0!=a.indexOf("/")&&(a="/"+a);return a},jc=function(a){a=kc(a);return"/"==a?1:a.split("/").length},le=function(a){a.ta&&J(77);a.na&&J(74);a.pa&&J(73);a.ua&&J(69)};function Xc(a,b,c){"none"==b&&(b="");var d=[],e=Ca(a);a="__utma"==a?6:2;for(var g=0;g<e.length;g++){var ca=(""+e[g]).split(".");ca.length>=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<b.length;d++)if(b[d].hash==c||b[d].hash==a)return b[d]};var od=new RegExp(/^https?:\/\/([^\/:]+)/),pd=/(.*)([?&#])(?:_ga=[^&#]*)(?:&?)(.*)/,me=/(.*)([?&#])(?:_gac=[^&#]*)(?:&?)(.*)/;function Bc(a){var b=a.get(Q),c=a.get(I)||"";b="_ga=2."+K(pa(c+b,0)+"."+c+"-"+b);if((c=a.get(ce))&&a.get(je)){var d=R(a,fe);1E3*d+R(a,he)<=(new Date).getTime()?(J(76),a=""):(J(44),a="&_gac=1."+K([pa(c,0),d,c].join(".")))}else a="";return b+a}
|
||||
function Ic(a,b){var c=new Date,d=O.navigator,e=d.plugins||[];a=[a,d.userAgent,c.getTimezoneOffset(),c.getYear(),c.getDate(),c.getHours(),c.getMinutes()+b];for(b=0;b<e.length;++b)a.push(e[b].description);return La(a.join("."))}function pa(a,b){var c=new Date,d=O.navigator,e=c.getHours()+Math.floor((c.getMinutes()+b)/60);return La([a,d.userAgent,d.language||"",c.getTimezoneOffset(),c.getYear(),c.getDate()+Math.floor(e/24),(24+e)%24,(60+c.getMinutes()+b)%60].join("."))}
|
||||
var Dc=function(a){J(48);this.target=a;this.T=!1};Dc.prototype.ca=function(a,b){if(a.tagName){if("a"==a.tagName.toLowerCase()){a.href&&(a.href=qd(this,a.href,b));return}if("form"==a.tagName.toLowerCase())return rd(this,a)}if("string"==typeof a)return qd(this,a,b)};
|
||||
var qd=function(a,b,c){var d=pd.exec(b);d&&3<=d.length&&(b=d[1]+(d[3]?d[2]+d[3]:""));(d=me.exec(b))&&3<=d.length&&(b=d[1]+(d[3]?d[2]+d[3]:""));a=a.target.get("linkerParam");var e=b.indexOf("?");d=b.indexOf("#");c?b+=(-1==d?"#":"&")+a:(c=-1==e?"?":"&",b=-1==d?b+(c+a):b.substring(0,d)+c+a+b.substring(d));b=b.replace(/&+_ga=/,"&_ga=");return b=b.replace(/&+_gac=/,"&_gac=")},rd=function(a,b){if(b&&b.action)if("get"==b.method.toLowerCase()){a=a.target.get("linkerParam").split("&");for(var c=0;c<a.length;c++){var d=
|
||||
a[c].split("="),e=d[1];d=d[0];for(var g=b.childNodes||[],ca=!1,l=0;l<g.length;l++)if(g[l].name==d){g[l].setAttribute("value",e);ca=!0;break}ca||(g=M.createElement("input"),g.setAttribute("type","hidden"),g.setAttribute("name",d),g.setAttribute("value",e),b.appendChild(g))}}else"post"==b.method.toLowerCase()&&(b.action=qd(a,b.action))};
|
||||
Dc.prototype.S=function(a,b,c){function d(c){try{c=c||O.event;a:{var d=c.target||c.srcElement;for(c=100;d&&0<c;){if(d.href&&d.nodeName.match(/^a(?:rea)?$/i)){var g=d;break a}d=d.parentNode;c--}g={}}("http:"==g.protocol||"https:"==g.protocol)&&sd(a,g.hostname||"")&&g.href&&(g.href=qd(e,g.href,b))}catch(k){J(26)}}var e=this;this.T||(this.T=!0,L(M,"mousedown",d,!1),L(M,"keyup",d,!1));c&&L(M,"submit",function(b){b=b||O.event;if((b=b.target||b.srcElement)&&b.action){var c=b.action.match(od);c&&sd(a,c[1])&&
|
||||
rd(e,b)}})};function sd(a,b){if(b==M.location.hostname)return!1;for(var c=0;c<a.length;c++)if(a[c]instanceof RegExp){if(a[c].test(b))return!0}else if(0<=b.indexOf(a[c]))return!0;return!1}function ke(a,b){return b!=Ic(a,0)&&b!=Ic(a,-1)&&b!=Ic(a,-2)&&b!=pa(a,0)&&b!=pa(a,-1)&&b!=pa(a,-2)};var p=/^(GTM|OPT)-[A-Z0-9]+$/,q=/;_gaexp=[^;]*/g,r=/;((__utma=)|([^;=]+=GAX?\d+\.))[^;]*/g,Aa=/^https?:\/\/[\w\-.]+\.google.com(:\d+)?\/optimize\/opt-launch\.html\?.*$/,t=function(a){function b(a,b){b&&(c+="&"+a+"="+K(b))}var c="https://www.google-analytics.com/gtm/js?id="+K(a.id);"dataLayer"!=a.B&&b("l",a.B);b("t",a.target);b("cid",a.clientId);b("cidt",a.ka);b("gac",a.la);b("aip",a.ia);a.sync&&b("m","sync");b("cycle",a.G);a.qa&&b("gclid",a.qa);Aa.test(M.referrer)&&b("cb",String(hd()));return c};var Jd=function(a,b,c){this.aa=b;(b=c)||(b=(b=P(a,V))&&"t0"!=b?Wd.test(b)?"_gat_"+Cc(P(a,Na)):"_gat_"+Cc(b):"_gat");this.Y=b;this.ra=null},Rd=function(a,b){var c=b.get(Wb);b.set(Wb,function(b){Pd(a,b,ed);Pd(a,b,ia);var d=c(b);Qd(a,b);return d});var d=b.get(Xb);b.set(Xb,function(b){var c=d(b);if(se(b)){if(ne()!==H(a,b)){J(80);var e={U:re(a,b,1),google:re(a,b,2),count:0};pe("https://stats.g.doubleclick.net/j/collect",e.U,e)}else ta(re(a,b,0));b.set(ed,"",!0)}return c})},Pd=function(a,b,c){!1===b.get(Ud)||
|
||||
b.get(c)||("1"==Ca(a.Y)[0]?b.set(c,"",!0):b.set(c,""+hd(),!0))},Qd=function(a,b){se(b)&&zc(a.Y,"1",b.get(Yb),b.get(W),b.get(Na),6E4)},se=function(a){return!!a.get(ed)&&a.get(Ud)},re=function(a,b,c){var d=new ee,e=function(a){$a(a).F&&d.set($a(a).F,b.get(a))};e(hb);e(ib);e(Na);e(Q);e(ed);if(0==c||1==c)e(Ad),e(ia),e(I);d.set($a(ld).F,Td(b));var g="";d.map(function(a,b){g+=K(a)+"=";g+=K(""+b)+"&"});g+="z="+hd();0==c?g=a.aa+g:1==c?g="t=dc&aip=1&_r=3&"+g:2==c&&(g="t=sr&aip=1&_r=4&slf_rd=1&"+g);return g},
|
||||
H=function(a,b){null===a.ra&&(a.ra=1===Ed(b),a.ra&&J(33));return a.ra},Wd=/^gtm\d+$/;var fd=function(a,b){a=a.b;if(!a.get("dcLoaded")){var c=new $c(Dd(a));c.set(29);a.set(Gd,c.w);b=b||{};var d;b[U]&&(d=Cc(b[U]));b=new Jd(a,"https://stats.g.doubleclick.net/r/collect?t=dc&aip=1&_r=3&",d);Rd(b,a);a.set("dcLoaded",!0)}};var Sd=function(a){if(!a.get("dcLoaded")&&"cookie"==a.get(ac)){var b=new Jd(a);Pd(b,a,ed);Pd(b,a,ia);Qd(b,a);if(se(a)){var c=ne()!==H(b,a);a.set(Md,1,!0);c?(J(79),a.set(gd,oe()+"/j/collect",!0),a.set(qe,{U:re(b,a,1),google:re(b,a,2),count:0},!0)):a.set(gd,oe()+"/r/collect",!0)}}};var Lc=function(){var a=O.gaGlobal=O.gaGlobal||{};return a.hid=a.hid||hd()};var ad,bd=function(a,b,c){if(!ad){var d=M.location.hash;var e=O.name,g=/^#?gaso=([^&]*)/;if(e=(d=(d=d&&d.match(g)||e&&e.match(g))?d[1]:Ca("GASO")[0]||"")&&d.match(/^(?:!([-0-9a-z.]{1,40})!)?([-.\w]{10,1200})$/i))zc("GASO",""+d,c,b,a,0),window._udo||(window._udo=b),window._utcp||(window._utcp=c),a=e[1],wa("https://www.google.com/analytics/web/inpage/pub/inpage.js?"+(a?"prefix="+a+"&":"")+hd(),"_gasojs");ad=!0}};var wb=/^(UA|YT|MO|GP)-(\d+)-(\d+)$/,pc=function(a){function b(a,b){d.b.data.set(a,b)}function c(a,c){b(a,c);d.filters.add(a)}var d=this;this.b=new Ya;this.filters=new Ha;b(V,a[V]);b(Na,sa(a[Na]));b(U,a[U]);b(W,a[W]||xa());b(Yb,a[Yb]);b(Zb,a[Zb]);b(Hd,a[Hd]);b($b,a[$b]);b(Wc,a[Wc]);b(bc,a[bc]);b(cc,a[cc]);b(Ka,a[Ka]);b(dc,a[dc]);b(ec,a[ec]);b(ac,a[ac]);b(Ad,a[Ad]);b(n,a[n]);b(Kd,a[Kd]);b(je,a[je]);b(hb,1);b(ib,"j68");c(Qb,Ma);c(oa,ua);c(dd,cd);c(Rb,Oa);c(md,vb);c(Sb,nc);c(Uc,Yc);c(Tb,Ja);c(Vb,Ta);
|
||||
c(Vc,Hc);c(zd,yd);c(Ld,Sd);c(Wb,Pa);c(Xb,Sa);c(Cd,Fd(this));Kc(this.b);Jc(this.b,a[Q]);this.b.set(jb,Lc());bd(this.b.get(Na),this.b.get(W),this.b.get(Yb))},Jc=function(a,b){var c=P(a,U);a.data.set(la,"_ga"==c?"_gid":c+"_gid");if("cookie"==P(a,ac)){hc=!1;c=Ca(P(a,U));c=Xd(a,c);if(!c){c=P(a,W);var d=P(a,$b)||xa();c=Xc("__utma",d,c);void 0!=c?(J(10),c=c.O[1]+"."+c.O[2]):c=void 0}c&&(hc=!0);if(d=c&&!a.get(Hd))if(d=c.split("."),2!=d.length)d=!1;else if(d=Number(d[1])){var e=R(a,Zb);d=d+e<(new Date).getTime()/
|
||||
1E3}else d=!1;d&&(c=void 0);c&&(a.data.set(xd,c),a.data.set(Q,c),c=Ca(P(a,la)),(c=Xd(a,c))&&a.data.set(I,c));if(a.get(je)&&(c=a.get(ce),d=a.get(ie),!c||d&&"aw.ds"!=d)){c={};if(M){d=[];e=M.cookie.split(";");for(var g=/^\s*_gac_(UA-\d+-\d+)=\s*(.+?)\s*$/,ca=0;ca<e.length;ca++){var l=e[ca].match(g);l&&d.push({ja:l[1],value:l[2]})}e={};if(d&&d.length)for(g=0;g<d.length;g++)(ca=d[g].value.split("."),"1"!=ca[0]||3!=ca.length)?c&&(c.na=!0):ca[1]&&(e[d[g].ja]?c&&(c.pa=!0):e[d[g].ja]=[],e[d[g].ja].push({timestamp:ca[1],
|
||||
qa:ca[2]}));d=e}else d={};d=d[P(a,Na)];le(c);d&&0!=d.length&&(c=d[0],a.data.set(fe,c.timestamp),a.data.set(ce,c.qa))}}if(a.get(Hd))a:if(d=be("_ga",a.get(cc)))if(a.get(bc))if(c=d.indexOf("."),-1==c)J(22);else{e=d.substring(0,c);g=d.substring(c+1);c=g.indexOf(".");d=g.substring(0,c);g=g.substring(c+1);if("1"==e){if(c=g,ke(c,d)){J(23);break a}}else if("2"==e){c=g.indexOf("-");e="";0<c?(e=g.substring(0,c),c=g.substring(c+1)):c=g.substring(1);if(ke(e+c,d)){J(53);break a}e&&(J(2),a.data.set(I,e))}else{J(22);
|
||||
break a}J(11);a.data.set(Q,c);if(c=be("_gac",a.get(cc)))c=c.split("."),"1"!=c[0]||4!=c.length?J(72):ke(c[3],c[1])?J(71):(a.data.set(ce,c[3]),a.data.set(fe,c[2]),J(70))}else J(21);b&&(J(9),a.data.set(Q,K(b)));a.get(Q)||((b=(b=O.gaGlobal&&O.gaGlobal.vid)&&-1!=b.search(jd)?b:void 0)?(J(17),a.data.set(Q,b)):(J(8),a.data.set(Q,ra())));a.get(I)||(J(3),a.data.set(I,ra()));mc(a)},Kc=function(a){var b=O.navigator,c=O.screen,d=M.location;a.set(lb,ya(a.get(ec),a.get(Kd)));if(d){var e=d.pathname||"";"/"!=e.charAt(0)&&
|
||||
(J(31),e="/"+e);a.set(kb,d.protocol+"//"+d.hostname+e+d.search)}c&&a.set(qb,c.width+"x"+c.height);c&&a.set(pb,c.colorDepth+"-bit");c=M.documentElement;var g=(e=M.body)&&e.clientWidth&&e.clientHeight,ca=[];c&&c.clientWidth&&c.clientHeight&&("CSS1Compat"===M.compatMode||!g)?ca=[c.clientWidth,c.clientHeight]:g&&(ca=[e.clientWidth,e.clientHeight]);c=0>=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;c<b.length;++c)(D(b[c],"utm_id")||D(b[c],"utm_campaign")||D(b[c],"utm_source")||D(b[c],"utm_medium")||D(b[c],"utm_term")||D(b[c],"utm_content")||D(b[c],"gclid")||D(b[c],"dclid")||D(b[c],"gclsrc"))&&d.push(b[c]);0<d.length&&(b="#"+d.join("&"),a.set(kb,
|
||||
a.get(kb)+b))}};pc.prototype.get=function(a){return this.b.get(a)};pc.prototype.set=function(a,b){this.b.set(a,b)};var qc={pageview:[mb],event:[ub,xb,yb,zb],social:[Bb,Cb,Db],timing:[Mb,Nb,Pb,Ob]};pc.prototype.send=function(a){if(!(1>arguments.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;c<b.length;c++)b[c]();A.set(a,[])},B=function(a){var b=ae(M.location.href);if(D(a.url,"https://www.google-analytics.com/gtm/js?id="))return!0;if(a.query||0<=a.url.indexOf("?")||0<=a.path.indexOf("://"))return!1;if(a.host==b.host&&a.port==b.port)return!0;b="http:"==a.protocol?80:443;return"www.google-analytics.com"==
|
||||
a.host&&(a.port||b)==b&&D(a.path,"/plugins/")?!0:!1},ae=function(a){function b(a){var b=(a.hostname||"").split(":")[0].toLowerCase(),c=(a.protocol||"").toLowerCase();c=1*a.port||("http:"==c?80:"https:"==c?443:"");a=a.pathname||"";D(a,"/")||(a="/"+a);return[b,""+c,a]}var c=M.createElement("a");c.href=M.location.href;var d=(c.protocol||"").toLowerCase(),e=b(c),g=c.search||"",ca=d+"//"+e[0]+(e[1]?":"+e[1]:"");D(a,"//")?a=d+a:D(a,"/")?a=ca+a:!a||D(a,"?")?a=ca+e[2]+(a||g):0>a.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=[];0<b.length&&!Z.v(b[0])&&!(b.shift(),0<Z.f.length););Z.f=Z.f.concat(b)};Z.J=function(a){for(var b=[],c=0;c<arguments.length;c++)try{var d=new sc(arguments[c]);d.g?C(d.a[0],d.a[1]):(d.i&&(d.ha=y(d.c,d.a[0],d.X,d.W)),b.push(d))}catch(e){}return b};
|
||||
Z.v=function(a){try{if(a.u)a.u.call(O,N.j("t0"));else{var b=a.c==gb?N:N.j(a.c);if(a.A){if("t0"==a.c&&(b=N.create.apply(N,a.a),null===b))return!0}else if(a.ba)N.remove(a.c);else if(b)if(a.i){if(a.ha&&(a.ha=y(a.c,a.a[0],a.X,a.W)),!u(a.a[0],b,a.W))return!0}else if(a.K){var c=a.C,d=a.a,e=b.plugins_.get(a.K);e[c].apply(e,d)}else b[a.C].apply(b,a.a)}}catch(g){}};var N=function(a){J(1);Z.D.apply(Z,[arguments])};N.h={};N.P=[];N.L=0;N.answer=42;var uc=[Na,W,V];
|
||||
N.create=function(a){var b=za(uc,[].slice.call(arguments));b[V]||(b[V]="t0");var c=""+b[V];if(N.h[c])return N.h[c];a:{if(b[Kd]){J(67);if(b[ac]&&"cookie"!=b[ac]){var d=!1;break a}if(void 0!==Ab)b[Q]||(b[Q]=Ab);else{b:{d=String(b[W]||xa());var e=String(b[Yb]||"/"),g=Ca(String(b[U]||"_ga"));d=na(g,d,e);if(!d||jd.test(d))d=!0;else if(d=Ca("AMP_TOKEN"),0==d.length)d=!0;else{if(1==d.length&&(d=decodeURIComponent(d[0]),"$RETRIEVING"==d||"$OPT_OUT"==d||"$ERROR"==d||"$NOT_FOUND"==d)){d=!0;break b}d=!1}}if(d&&
|
||||
tc(ic,String(b[Na]))){d=!0;break a}}}d=!1}if(d)return null;b=new pc(b);N.h[c]=b;N.P.push(b);return b};N.remove=function(a){for(var b=0;b<N.P.length;b++)if(N.P[b].get(V)==a){N.P.splice(b,1);N.h[a]=null;break}};N.j=function(a){return N.h[a]};N.getAll=function(){return N.P.slice(0)};
|
||||
N.N=function(){"ga"!=gb&&J(49);var a=O[gb];if(!a||42!=a.answer){N.L=a&&a.l;N.loaded=!0;var b=O[gb]=N;X("create",b,b.create);X("remove",b,b.remove);X("getByName",b,b.j,5);X("getAll",b,b.getAll,6);b=pc.prototype;X("get",b,b.get,7);X("set",b,b.set,4);X("send",b,b.send);X("requireSync",b,b.ma);b=Ya.prototype;X("get",b,b.get);X("set",b,b.set);if("https:"!=M.location.protocol&&!Ba){a:{b=M.getElementsByTagName("script");for(var c=0;c<b.length&&100>c;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<a.length;b++)a[b].get(V)};var da=N.N,Nd=O[gb];Nd&&Nd.r?da():z(da);z(function(){Z.D(["provide","render",ua])});function La(a){var b=1,c;if(a)for(b=0,c=a.length-1;0<=c;c--){var d=a.charCodeAt(c);b=(b<<6&268435455)+d+(d<<14);d=b&266338304;b=0!=d?b^d>>21:b}return b};})(window);
|
||||
File diff suppressed because one or more lines are too long
@@ -1,150 +0,0 @@
|
||||
/**
|
||||
* Site-wide JS that sets up:
|
||||
*
|
||||
* [1] MathJax rendering on navigation
|
||||
* [2] Sidebar toggling
|
||||
* [3] Sidebar scroll preserving
|
||||
* [4] Keyboard navigation
|
||||
* [5] Right sidebar scroll highlighting
|
||||
*/
|
||||
|
||||
const togglerId = 'js-sidebar-toggle'
|
||||
const textbookId = 'js-textbook'
|
||||
const togglerActiveClass = 'is-active'
|
||||
const textbookActiveClass = 'js-show-sidebar'
|
||||
const mathRenderedClass = 'js-mathjax-rendered'
|
||||
const icon_path = document.location.origin + `${site_basename}assets`;
|
||||
|
||||
const getToggler = () => 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);
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -17,38 +17,31 @@
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"Migration Compatibility Assessment\n",
|
||||
"=======================================================\n",
|
||||
"# Migration Compatibility Assessment\n",
|
||||
"Use dmacmd.exe to assess databases in an unattended mode, and output the result to JSON or CSV file. This method is especially useful when assessing several databases or huge databases.\n",
|
||||
"\n",
|
||||
"Description\n",
|
||||
"-----------\n",
|
||||
"Use this notebook to analzye an on-premises SQL Server instance or database for compatibility for migration to SQL Azure. The assessment will provide guidance on features not currently supported in Azure and remediation actions that can be taken to prepare for migration.\n",
|
||||
""
|
||||
"## Notebook Variables\n",
|
||||
"\n",
|
||||
"| Line | Variable | Description |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| 1 | ExecutableFile | Path to DmaCmd.exe file, usually _\"C:\\\\Program Files\\\\Microsoft Data Migration Assistant\\\\DmaCmd.exe\"_ if installed to default location |\n",
|
||||
"| 2 | AssessmentName | Unique name for assessment |\n",
|
||||
"| 3 | Server | Target SQL Server |\n",
|
||||
"| 4 | InitialCatalog | Name of the database for the specified server |\n",
|
||||
"| 5 | ResultPath | Path and name of the file to store results in json format |"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "6764dd37-fb1f-400d-8f2b-70bc36fc3b61"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"This notebook requires Data Migration Assistant to be installed in order to execute the below commands.\r\n",
|
||||
"The installtion link would be [Data Migration Assistant download](https://www.microsoft.com/en-us/download/confirmation.aspx?id=53595)\r\n",
|
||||
"\r\n",
|
||||
"_With version 2.1 and above, when installation of Data Migration Assistant is successfull, it will also install dmacmd.exe in %ProgramFiles%\\Microsoft Data Migration Assistant\\. Use dmacmd.exe to assess databases in an unattended mode, and output the result to JSON or CSV file. This method is especially useful when assessing several databases or huge databases_"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "68506e39-d34b-4f17-a0c6-94e978f76488"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"$ExecutableFile = \"\" # Path of the DmaCmd.exe file, generally the path would be \"C:\\Program Files\\Microsoft Data Migration Assistant\\DmaCmd.exe\"\r\n",
|
||||
"$AssessmentName = \"\" # Name of the Assessment\r\n",
|
||||
"$Server = \"\" # Targert Sql Server\r\n",
|
||||
"$InitialCatalog = \"\" # Database name of the specified Sql Server\r\n",
|
||||
"$ResultPath = \"\" # Path and Name of the file to store the result in json format, for example \"C:\\\\temp\\\\Results\\\\AssessmentReport.json\""
|
||||
"$ExecutableFile = \"C:\\Program Files\\Microsoft Data Migration Assistant\\DmaCmd.exe\" # Update if different\r\n",
|
||||
"$AssessmentName = \"\"\r\n",
|
||||
"$Server = \"\"\r\n",
|
||||
"$InitialCatalog = \"\"\r\n",
|
||||
"$ResultPath = \"\""
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "d81972c1-3b0b-47d9-b8a3-bc5ab4001a34"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# Assessments
|
||||
|
||||
|
||||
[Home](../readme.md)
|
||||
|
||||
## Notebooks in this Chapter
|
||||
- [SQL Server Best Practices Assessment](sql-server-assessment.ipynb) - Use the SQL Server Assessment API to review the configuration of instances by name or dynamically by specifying the instance of a Central Management Server. SQL Assessment API provides a mechanism to evaluate the configuration of your SQL Server for best practices. The API is delivered with a ruleset containing best practice rules suggested by SQL Server Team. This ruleset is enhancing with the release of new versions but at the same time, the API is built with the intent to give a highly customizable and extensible solution. So, users can tune the default rules and create their own ones. SQL Assessment API is useful when you want to make sure your SQL Server configuration is in line with recommended best practices. After an initial assessment, configuration stability can be tracked by regularly scheduled assessments.
|
||||
Preparing for the cloud requires a crawl-walk-run mentality. The first step, or crawl, towards hybrid migration is determining the fitness of existing on-premise resources. An assessment is an analysis performed against a chosen SQL Server object such as a Server or Database instance. It is recommended to fix any issues found by the analysis prior to migrating a database from on-premise to Azure.
|
||||
|
||||
- [Compatibility Assessment](compatibility-assessment.ipynb) - Coming soon
|
||||
## Notebooks in this Chapter
|
||||
- [SQL Server Best Practices Assessment](sql-server-assessment.ipynb) - demonstrates the use of the [SQL Server Assessment API](https://docs.microsoft.com/en-us/sql/sql-assessment-api/sql-assessment-api-overview), a tool to review the configuration of a SQL Server and Databases for best practices.
|
||||
|
||||
- [Compatibility Assessment](compatibility-assessment.ipynb) - Analzye an on-premises SQL Server instance or database for compatibility for migration to SQL Azure. The assessment will provide guidance on features not currently supported in Azure and remediation actions that can be taken to prepare for migration.
|
||||
@@ -19,17 +19,16 @@
|
||||
"source": [
|
||||
"# SQL Server Assessment Tool\n",
|
||||
"\n",
|
||||
"This notebook will demonstrate the use of the [SQL Server Assessment API](https://docs.microsoft.com/en-us/sql/sql-assessment-api/sql-assessment-api-overview), a tool to review the configuration of a SQL Server and Databases for best practices. An assessment is performed against a chosen SQL Server object. The default ruleset checks for two kinds of objects: Server and Database. In addition, the API supports Filegroup and AvailabilityGroup. When attempting to migrate a database from on-premise to Azure, it is recommended to fix any assessment items prior.\n",
|
||||
"\n",
|
||||
"**Unlike other notebooks, do not execute all cells of this notebook!** \n",
|
||||
"Unlike other notebooks, **do not execute all cells of this notebook!** \n",
|
||||
"\n",
|
||||
"A single assessment may take awhile so fill out the variables and execute the cell that matches the desired environment to perform the assessment needed. Only one of these cells needs to be executed after the variables are defined.\n",
|
||||
"\n",
|
||||
"1. Ensure that the proper APIs and modules are installed per the <a href=\"../prereqs.ipynb\">prerequisites</a> notebook\n",
|
||||
"2. Define a service instance and group corresponding to the SQL Server instances to be assessed\n",
|
||||
"3. Choose an example below that corresponds to the appropriate task\n",
|
||||
"4. Execute only that example's code block and wait for results\n",
|
||||
"5. Fix any recommended issues and rerun Assessment API until clear"
|
||||
"## Notebook Variables\n",
|
||||
"\n",
|
||||
"| Line | Variable | Description |\n",
|
||||
"| ---- | -------- | ----------- |\n",
|
||||
"| 1 | ServerInstance | Name of the SQL Server instance |\n",
|
||||
"| 2 | Group | (Optional) Name of the server group, if known | "
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "86ecfb01-8c38-4a99-92a8-687d8ec7f4b0"
|
||||
@@ -47,6 +46,20 @@
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"## Notebook Steps\r\n",
|
||||
"1. Ensure that the proper APIs and modules are installed per the <a href=\"../prereqs.ipynb\">prerequisites</a> notebook\r\n",
|
||||
"2. Define a service instance and group corresponding to the SQL Server instances to be assessed\r\n",
|
||||
"3. Choose an example below that corresponds to the appropriate task\r\n",
|
||||
"4. Execute only that example's code block and wait for results\r\n",
|
||||
"5. Fix any recommended issues and rerun Assessment API until clear"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "541f6806-f8d2-4fc5-a8fb-6d42947d1a64"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"name": "python3",
|
||||
"display_name": "Python 3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.6.6",
|
||||
"mimetype": "text/x-python",
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"pygments_lexer": "ipython3",
|
||||
"nbconvert_exporter": "python",
|
||||
"file_extension": ".py"
|
||||
}
|
||||
},
|
||||
"nbformat_minor": 2,
|
||||
"nbformat": 4,
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"import pandas,sys,os,getpass,json,html,time\r\n",
|
||||
"from string import Template"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "1887c716-6e0c-41d1-9d67-cfa93884c0d6"
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": 1
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"sql_password = \"\"\r\n",
|
||||
"sql_port = \"\""
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "f3de6ea8-1ea8-43d6-9277-836b57d85845"
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": 2
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"from IPython.display import *\r\n",
|
||||
"connectionParameter = '{\"serverName\":\"localhost,' + sql_port + '\",\"providerName\":\"MSSQL\",\"authenticationType\":\"SqlLogin\",\"userName\":\"sa\",\"password\":' + json.dumps(sql_password) + '}'\r\n",
|
||||
"display(HTML('<br/><a href=\"command:azdata.connect?' + html.escape(connectionParameter)+'\"><font size=\"3\">Click here to connect to SQL Server</font></a><br/>'))\r\n",
|
||||
"display(HTML('<br/><span style=\"color:red\"><font size=\"2\">NOTE: The SQL Server password is included in this link, you may want to clear the results of this code cell before saving the notebook.</font></span>'))"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "8e5e0b41-a27d-4a73-9ba6-c0d3bd7a9a2f"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": "<IPython.core.display.HTML object>",
|
||||
"text/html": "<br/><a href=\"command:azdata.connect?{"serverName":"localhost,","providerName":"MSSQL","authenticationType":"SqlLogin","userName":"sa","password":""}\"><font size=\"3\">Click here to connect to SQL Server</font></a><br/>"
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}, {
|
||||
"data": {
|
||||
"text/plain": "<IPython.core.display.HTML object>",
|
||||
"text/html": "<br/><span style=\"color:red\"><font size=\"2\">NOTE: The SQL Server password is included in this link, you may want to clear the results of this code cell before saving the notebook.</font></span>"
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"execution_count": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
# Appendices
|
||||
[Home](readme.md)
|
||||
|
||||
## Appendix: Locations
|
||||
See the <a href="https://azure.microsoft.com/en-us/global-infrastructure/locations/">Azure locations</a> page for a complete list of Azure regions along with their general physical location. The following is a list of common North American location settings for this guide:
|
||||
|
||||
### US Regions
|
||||
### Regions
|
||||
| Setting | Location |
|
||||
| ------------ | --------- |
|
||||
| Central US | Iowa |
|
||||
|
||||
@@ -18,12 +18,46 @@
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"# Export Existing Azure SQL Server Resources\r\n",
|
||||
"Export notebook that will utilize the ADP resources\r\n",
|
||||
"\r\n",
|
||||
"\r\n",
|
||||
"<!-- Disable bullets to be shown for checkbox markup -->\r\n",
|
||||
"<style type=\"text/css\">\r\n",
|
||||
" ul { list-style-type: none }\r\n",
|
||||
"</style>\r\n",
|
||||
"## Notebook Variables\r\n",
|
||||
"| Line | Variable | Description |\r\n",
|
||||
"| -- | -- | -- |\r\n",
|
||||
"| 1 | AdpSubscription | Azure Subscription ID/Name for the ADP Resource Group # Both RG are assumed to be in the same subscription |\r\n",
|
||||
"| 2 | AdpResourceGroup | Azure Resource Group which contains the ADP Resources | \r\n",
|
||||
"| 3 | SourceResourceGroup | Azure ResourceGroup where the sql server to be exported exists | \r\n",
|
||||
"| 4 | LogicalSQLServerName | Logical sql server name of the sql server to be exported | \r\n",
|
||||
"| 5 | StorageAccount | target storage account to store exported files # any storage account, but must be in the same RG as the ADP resources | \r\n",
|
||||
"| 6 | AdpFunc | |\r\n",
|
||||
"| 7 | AdpBatch | | \r\n",
|
||||
"| 8 | AdpVNET | | "
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "b72d138a-566f-4161-b7a6-7264487e446c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"$AdpSubscription = \"\"\r\n",
|
||||
"$AdpResourceGroup = \"\"\r\n",
|
||||
"$SourceResourceGroup= \"\"\r\n",
|
||||
"$LogicalSQLServer = \"\"\r\n",
|
||||
"$StorageAccount = \"\"\r\n",
|
||||
"$AdpFunc = $AdpResourceGroup + \"Control\"\r\n",
|
||||
"$AdpBatch = $AdpResourceGroup.ToLower() + \"batch\"\r\n",
|
||||
"$AdpVNET = $AdpResourceGroup + \"Vnet\""
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "417edc0e-1107-4a27-a4cf-e921f79b3f6a",
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"## Steps\r\n",
|
||||
"Gather input:\r\n",
|
||||
"* [ ] Connect to Azure Subscription\r\n",
|
||||
@@ -45,39 +79,6 @@
|
||||
"azdata_cell_guid": "a9da248a-20f1-4574-bd04-7324e70c05a3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"## Set Variables for the Notebook"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "b72d138a-566f-4161-b7a6-7264487e446c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"# ADP Resource \r\n",
|
||||
"$Env:BOOTSTRAP_Subscription = \"\" # Azure Subscription ID/Name for the ADP Resource Group # Both RG are assumed to be in the same subscription\r\n",
|
||||
"$Env:BOOTSTRAP_ResourceGroup = \"\" # Azure Resource Group which contains the ADP Resources\r\n",
|
||||
"\r\n",
|
||||
"# SQL Server \r\n",
|
||||
"$SourceResourceGroupName = \"\" # Azure ResourceGroup where the sql server to be exported exists\r\n",
|
||||
"$LogicalSQLServerName = \"\" # Logical sql server name of the sql server to be exported\r\n",
|
||||
"$StorageAccount = \"\" # target storage account to store exported files # any storage account, but must be in the same RG as the ADP resources.\r\n",
|
||||
"\r\n",
|
||||
"# Set Variables for ADP Resources\r\n",
|
||||
"$Env:BOOTSTRAP_FUNC = $Env:BOOTSTRAP_ResourceGroup + \"Control\"\r\n",
|
||||
"$Env:BOOTSTRAP_BATCH = $Env:BOOTSTRAP_ResourceGroup.ToLower() + \"batch\"\r\n",
|
||||
"$Env:BOOTSTRAP_VNET = $Env:BOOTSTRAP_ResourceGroup + \"Vnet\""
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "417edc0e-1107-4a27-a4cf-e921f79b3f6a",
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
@@ -105,9 +106,9 @@
|
||||
" $subscriptions = az account list -o JSON | ConvertFrom-Json # getting subscriptions for the user to use in gridview\r\n",
|
||||
" }\r\n",
|
||||
"\r\n",
|
||||
" if(![string]::IsNullOrWhiteSpace($Env:BOOTSTRAP_Subscription)) #If there is a subscription specified by user in the variables section\r\n",
|
||||
" if(![string]::IsNullOrWhiteSpace($AdpSubscription)) #If there is a subscription specified by user in the variables section\r\n",
|
||||
" {\r\n",
|
||||
" $specified_Subscription= az account show --subscription $Env:BOOTSTRAP_Subscription -o json |ConvertFrom-Json \r\n",
|
||||
" $specified_Subscription= az account show --subscription $AdpSubscription -o json |ConvertFrom-Json \r\n",
|
||||
" if (!$specified_Subscription) #if specified subscription is not valid\r\n",
|
||||
" { \r\n",
|
||||
" $currentUser= az ad signed-in-user show --query \"{displayName:displayName,UPN:userPrincipalName}\" -o json|ConvertFrom-Json # get current logged in user infomration\r\n",
|
||||
@@ -123,8 +124,8 @@
|
||||
" $selectedSubscription = $subscriptions | Select-Object -Property Name, Id | Out-GridView -PassThru\r\n",
|
||||
" $SubscriptionId = $selectedSubscription.Id\r\n",
|
||||
" $Subscription = $selectedSubscription.Name \r\n",
|
||||
" $Env:BOOTSTRAP_Subscription = $subscription \r\n",
|
||||
" Write-Output \"Using subscription... '$Env:BOOTSTRAP_Subscription' ... '$SubscriptionId'\" \r\n",
|
||||
" $AdpSubscription = $subscription \r\n",
|
||||
" Write-Output \"Using subscription... '$AdpSubscription' ... '$SubscriptionId'\" \r\n",
|
||||
" } \r\n",
|
||||
"}\r\n",
|
||||
"\r\n",
|
||||
@@ -369,8 +370,8 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"Verify-ADPResources -Subscription $Env:BOOTSTRAP_Subscription -ADPResourceGroupName $Env:BOOTSTRAP_ResourceGroup `\r\n",
|
||||
" -BatchAccountName $Env:BOOTSTRAP_BATCH -FunctionName $Env:BOOTSTRAP_FUNC -VNetName $Env:BOOTSTRAP_VNET "
|
||||
"Verify-ADPResources -Subscription $AdpSubscription -ADPResourceGroupName $AdpResourceGroup `\r\n",
|
||||
" -BatchAccountName $AdpBatch -FunctionName $AdpFunc -VNetName $AdpVNET "
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "8185f2ea-d368-42c5-9246-bc1871affc63"
|
||||
@@ -391,7 +392,7 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"Provision-FuncRBAC -FunctionName $Env:BOOTSTRAP_FUNC -ScopeRGName $SourceResourceGroupName -ResourceGroupName $Env:BOOTSTRAP_ResourceGroup -Subscription $Env:BOOTSTRAP_Subscription"
|
||||
"Provision-FuncRBAC -FunctionName $AdpFunc -ScopeRGName $SourceResourceGroup -ResourceGroupName $AdpResourceGroup -Subscription $AdpSubscription"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "7678701e-ec40-43d9-baff-fd1cdabba1cd"
|
||||
@@ -413,7 +414,7 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"$sqlServer = az sql server show --name $LogicalSQLServerName --resource-group $SourceResourceGroupName --subscription $Env:BOOTSTRAP_Subscription -o JSON | ConvertFrom-JSON\r\n",
|
||||
"$sqlServer = az sql server show --name $LogicalSQLServerName --resource-group $SourceResourceGroup --subscription $AdpSubscription -o JSON | ConvertFrom-JSON\r\n",
|
||||
"if ($sqlServer)\r\n",
|
||||
"{\r\n",
|
||||
" Write-Host \"Source SQL Server: \" $sqlServer.name\r\n",
|
||||
@@ -426,7 +427,7 @@
|
||||
" Write-Host \"ERROR: Source server is not in Ready state. Current state is: \" $sqlServer.state\r\n",
|
||||
" }\r\n",
|
||||
"\r\n",
|
||||
" $sqlAzureAdmin = az sql server ad-admin list --server $LogicalSQLServerName --resource-group $SourceResourceGroupName --subscription $Env:BOOTSTRAP_Subscription -o JSON | ConvertFrom-JSON\r\n",
|
||||
" $sqlAzureAdmin = az sql server ad-admin list --server $LogicalSQLServerName --resource-group $SourceResourceGroup --subscription $AdpSubscription -o JSON | ConvertFrom-JSON\r\n",
|
||||
" if ($sqlAzureAdmin)\r\n",
|
||||
" {\r\n",
|
||||
" Write-Host \"Azure AD admin set to\" $sqlAzureAdmin.login\r\n",
|
||||
@@ -442,8 +443,8 @@
|
||||
"{\r\n",
|
||||
" Write-Host \"ERROR: Source server \" $sqlServer.name \"not found or current account lacks access to resource.\"\r\n",
|
||||
" Write-Host \"Validate input settings:\"\r\n",
|
||||
" Write-Host \"Resource group: \" $SourceResourceGroupName\r\n",
|
||||
" Write-Host \"Subscription: \" $Env:BOOTSTRAP_Subscription\r\n",
|
||||
" Write-Host \"Resource group: \" $SourceResourceGroup\r\n",
|
||||
" Write-Host \"Subscription: \" $AdpSubscription\r\n",
|
||||
"}"
|
||||
],
|
||||
"metadata": {
|
||||
@@ -464,8 +465,8 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"$InputForExportFunction = Prepare-InputForExportFunction -Subscription $Env:BOOTSTRAP_Subscription -ADPResourceGroupName $Env:BOOTSTRAP_ResourceGroup `\r\n",
|
||||
" -BatchAccountName $Env:BOOTSTRAP_BATCH -FunctionName $Env:BOOTSTRAP_FUNC -VNetName $Env:BOOTSTRAP_VNET -SourceRGName $SourceResourceGroupName `\r\n",
|
||||
"$InputForExportFunction = Prepare-InputForExportFunction -Subscription $AdpSubscription -ADPResourceGroupName $AdpResourceGroup `\r\n",
|
||||
" -BatchAccountName $AdpBatch -FunctionName $AdpFunc -VNetName $AdpVNET -SourceRGName $SourceResourceGroup `\r\n",
|
||||
" -SqlServerName $LogicalSQLServerName -StorageAccountName $StorageAccount\r\n",
|
||||
"Write-Host \"Setting parameter variables for Export Function Call...\"\r\n",
|
||||
"$InputForExportFunction.Header\r\n",
|
||||
@@ -528,7 +529,7 @@
|
||||
" Write-Host \"`tCreated Export Batch Job ID: \" $batchJobId\r\n",
|
||||
" Write-Host \"`tExport container URL: \" $containerUrl\r\n",
|
||||
"\r\n",
|
||||
" $azBatchLogin = az batch account login --name $Env:BOOTSTRAP_BATCH --resource-group $Env:BOOTSTRAP_ResourceGroup -o JSON | ConvertFrom-Json\r\n",
|
||||
" $azBatchLogin = az batch account login --name $AdpBatch --resource-group $AdpResourceGroup -o JSON | ConvertFrom-Json\r\n",
|
||||
" $jobStatus = az batch job show --job-id $batchJobID -o JSON | ConvertFrom-Json\r\n",
|
||||
" Write-Host \"Export Job running on Pool: \" $jobStatus.poolInfo.poolId\r\n",
|
||||
" Write-Host \"`tExport Request Status: \" $jobStatus.state\r\n",
|
||||
|
||||
@@ -63,8 +63,8 @@
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"# ADP Resource \r\n",
|
||||
"$Env:BOOTSTRAP_Subscription = \"\" # Azure Subscription ID/Name # The bacpac files and ADP Resources are assumed to be in the same subscription\r\n",
|
||||
"$Env:BOOTSTRAP_ResourceGroup = \"\" # Azure Resource Group which contains the ADP Resources\r\n",
|
||||
"$AdpSubscription = \"\" # Azure Subscription ID/Name # The bacpac files and ADP Resources are assumed to be in the same subscription\r\n",
|
||||
"$AdpResourceGroup = \"\" # Azure Resource Group which contains the ADP Resources\r\n",
|
||||
"\r\n",
|
||||
"# SQL Server \r\n",
|
||||
"$TargetResourceGroupName = \"\" # Azure ResourceGroup into which the sql server backup needs to be restored\r\n",
|
||||
@@ -74,9 +74,9 @@
|
||||
"$LSqlServerPassword = \"\"\r\n",
|
||||
"\r\n",
|
||||
"# Set Variables for ADP Resources\r\n",
|
||||
"$Env:BOOTSTRAP_FUNC = $Env:BOOTSTRAP_ResourceGroup + \"Control\" \r\n",
|
||||
"$Env:BOOTSTRAP_BATCH = $Env:BOOTSTRAP_ResourceGroup.ToLower() + \"batch\"\r\n",
|
||||
"$Env:BOOTSTRAP_VNET = $Env:BOOTSTRAP_ResourceGroup + \"Vnet\""
|
||||
"$AdpFunc = $AdpResourceGroup + \"Control\" \r\n",
|
||||
"$AdpBatch = $AdpResourceGroup.ToLower() + \"batch\"\r\n",
|
||||
"$AdpVNET = $AdpResourceGroup + \"Vnet\""
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "01888595-0d1c-445b-ba85-dd12caa30192",
|
||||
@@ -112,9 +112,9 @@
|
||||
" $subscriptions = az account list -o JSON | ConvertFrom-Json # getting subscriptions for the user to use in gridview\r\n",
|
||||
" }\r\n",
|
||||
"\r\n",
|
||||
" if(![string]::IsNullOrWhiteSpace($Env:BOOTSTRAP_Subscription)) #If there is a subscription specified by user in the variables section\r\n",
|
||||
" if(![string]::IsNullOrWhiteSpace($AdpSubscription)) #If there is a subscription specified by user in the variables section\r\n",
|
||||
" {\r\n",
|
||||
" $specified_Subscription= az account show --subscription $Env:BOOTSTRAP_Subscription -o json |ConvertFrom-Json \r\n",
|
||||
" $specified_Subscription= az account show --subscription $AdpSubscription -o json |ConvertFrom-Json \r\n",
|
||||
" if (!$specified_Subscription) #if specified subscription is not valid\r\n",
|
||||
" { \r\n",
|
||||
" $currentUser= az ad signed-in-user show --query \"{displayName:displayName,UPN:userPrincipalName}\" -o json|ConvertFrom-Json # get current logged in user infomration\r\n",
|
||||
@@ -130,8 +130,8 @@
|
||||
" $selectedSubscription = $subscriptions | Select-Object -Property Name, Id | Out-GridView -PassThru\r\n",
|
||||
" $SubscriptionId = $selectedSubscription.Id\r\n",
|
||||
" $Subscription = $selectedSubscription.Name \r\n",
|
||||
" $Env:BOOTSTRAP_Subscription = $subscription \r\n",
|
||||
" Write-Output \"Using subscription... '$Env:BOOTSTRAP_Subscription' ... '$SubscriptionId'\" \r\n",
|
||||
" $AdpSubscription = $subscription \r\n",
|
||||
" Write-Output \"Using subscription... '$AdpSubscription' ... '$SubscriptionId'\" \r\n",
|
||||
" } \r\n",
|
||||
"}\r\n",
|
||||
"\r\n",
|
||||
@@ -381,8 +381,8 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"Verify-ADPResources -Subscription $Env:BOOTSTRAP_Subscription -ADPResourceGroupName $Env:BOOTSTRAP_ResourceGroup `\r\n",
|
||||
" -BatchAccountName $Env:BOOTSTRAP_BATCH -FunctionName $Env:BOOTSTRAP_FUNC -VNetName $Env:BOOTSTRAP_VNET "
|
||||
"Verify-ADPResources -Subscription $AdpSubscription -ADPResourceGroupName $AdpResourceGroup `\r\n",
|
||||
" -BatchAccountName $AdpBatch -FunctionName $AdpFunc -VNetName $AdpVNET "
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "e89f6eb9-fcbc-4b7d-bcd1-37f1eb52cc02",
|
||||
@@ -406,7 +406,7 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"Provision-FuncRBAC -FunctionName $Env:BOOTSTRAP_FUNC -ScopeRGName $TargetResourceGroupName -ResourceGroupName $Env:BOOTSTRAP_ResourceGroup -Subscription $Env:BOOTSTRAP_Subscription"
|
||||
"Provision-FuncRBAC -FunctionName $AdpFunc -ScopeRGName $TargetResourceGroupName -ResourceGroupName $AdpResourceGroup -Subscription $AdpSubscription"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "c374e57c-51ec-4a3f-9966-1e50cefc8510"
|
||||
@@ -426,9 +426,9 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"$InputForImportFunction = Prepare-InputForImportFunction -Subscription $Env:BOOTSTRAP_Subscription -ADPResourceGroupName $Env:BOOTSTRAP_ResourceGroup `\r\n",
|
||||
" -BatchAccountName $Env:BOOTSTRAP_BATCH -FunctionName $Env:BOOTSTRAP_FUNC -TargetRGName $TargetResourceGroupName `\r\n",
|
||||
" -VNetName $Env:BOOTSTRAP_VNET -BackupFiles_StorageAccount $StorageAccountName -BackupFiles_ContainerName $ContainerName `\r\n",
|
||||
"$InputForImportFunction = Prepare-InputForImportFunction -Subscription $AdpSubscription -ADPResourceGroupName $AdpResourceGroup `\r\n",
|
||||
" -BatchAccountName $AdpBatch -FunctionName $AdpFunc -TargetRGName $TargetResourceGroupName `\r\n",
|
||||
" -VNetName $AdpVNET -BackupFiles_StorageAccount $StorageAccountName -BackupFiles_ContainerName $ContainerName `\r\n",
|
||||
" -SqlServerName $LogicalSQLServerName -SqlServerPassword $LSqlServerpassword\r\n",
|
||||
"Write-Host \"Setting parameter variables for Import Function Call...\"\r\n",
|
||||
"$InputForImportFunction.Header\r\n",
|
||||
@@ -495,7 +495,7 @@
|
||||
" $containerUrl = $outputParams.Item2[3]\r\n",
|
||||
"\r\n",
|
||||
" Write-Host \"`tCreated Import Batch Job ID: \" $batchJobId\r\n",
|
||||
" $azBatchLogin = az batch account login --name $Env:BOOTSTRAP_BATCH --resource-group $Env:BOOTSTRAP_ResourceGroup -o JSON | ConvertFrom-Json\r\n",
|
||||
" $azBatchLogin = az batch account login --name $AdpBatch --resource-group $AdpResourceGroup -o JSON | ConvertFrom-Json\r\n",
|
||||
" $jobStatus = az batch job show --job-id $batchJobID -o JSON | ConvertFrom-Json\r\n",
|
||||
" Write-Host \"Import Job running on Pool: \" $jobStatus.poolInfo.poolId\r\n",
|
||||
" Write-Host \"`Import Request Status: \" $jobStatus.state\r\n",
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
# Data Portability
|
||||
[Home](../readme.md)
|
||||
|
||||
Notebooks in this chapter perform a data migration using a custom Azure function that can be deployed to an Azure subscription. It enables [Azure Batch](https://azure.microsoft.com/en-us/services/batch) computing of a complex SQL Server migration to and from a single Resource Group. Azure Batch is a process that runs large-scale parallel and high-performance computing (HPC) batch jobs efficiently in Azure. This greatly reduces the processing required locally which should prevent long execution times, timeouts and retries. Importing and exporting data to and from Azure is supported for multiple SQL database instances. Data is imported and exported to and from standard SQL backup formats (*.bacpac) which "encapsulates the database schema as well as the data stored in the database" ([Microsoft Docs](https://docs.microsoft.com/en-us/sql/relational-databases/data-tier-applications/data-tier-applications)).
|
||||
|
||||
## Notebooks in this Chapter
|
||||
|
||||
- [Azure Data Portability Setup](bootstrap.ipynb) - Configure and install a custom Azure function to migrate data to and from Azure
|
||||
- [Azure Data Portability Setup](setup-adp.ipynb) - Configure and install a custom Azure function to migrate data to and from Azure <br/>
|
||||
<img width="25%" src="VisualBootstrapperNB.PNG"/>
|
||||
|
||||
- [Export Sql Server](export-sql-server.ipynb) - from SQL Azure to a standard SQL backup format
|
||||
|
||||
- [Import Sql Server](import-sql-server.ipynb) - from SQL backup format to Azure
|
||||
|
||||
The Notebooks in this chapter perform a data migration using a custom Azure function that can be deployed to an Azure subscription. It enables [Azure Batch](https://azure.microsoft.com/en-us/services/batch) computing of a complex SQL Server migration to and from a single Resource Group. Azure Batch is a process that runs large-scale parallel and high-performance computing (HPC) batch jobs efficiently in Azure. This greatly reduces the processing required locally which should prevent long execution times, timeouts and retries. Importing and exporting data to and from Azure is supported for multiple SQL database instances. Data is imported and exported to and from standard SQL backup formats (*.bacpac) which "encapsulates the database schema as well as the data stored in the database" ([Microsoft Docs](https://docs.microsoft.com/en-us/sql/relational-databases/data-tier-applications/data-tier-applications)).
|
||||
|
||||
## Steps
|
||||
1. The Azure function must first be deployed using the setup notebook
|
||||
2. Open the notebook for the desired migration path
|
||||
2. Open the notebook for the desired migration path (import or export)
|
||||
3. Configure and execute notebook
|
||||
4. Monitor progress with periodic notebook queries
|
||||
5. Verify data has been imported/exported by reviewing the storage account for the migrated Resource Group
|
||||
|
||||
@@ -55,22 +55,22 @@
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"# Setup client environment variables that the rest of the notebook will use\r\n",
|
||||
"$Env:BOOTSTRAP_ResourceGroup = \"\" # Target Resource Group to bootstrap with ADP components - A new one will be created if the specified Resource Group doesn't exist\r\n",
|
||||
"$Env:BOOTSTRAP_RG_REGION = \"eastus\" # Region/Location of the resource group to be bootstrapped\r\n",
|
||||
"$AdpResourceGroup = \"\" # Target Resource Group to bootstrap with ADP components - A new one will be created if the specified Resource Group doesn't exist\r\n",
|
||||
"$AdpRegion = \"eastus\" # Region/Location of the resource group to be bootstrapped\r\n",
|
||||
"\r\n",
|
||||
"# Derived settings\r\n",
|
||||
"$Env:BOOTSTRAP_Subscription = \"\" # Target Azure Subscription Name or ID to bootstrap data portability resources\r\n",
|
||||
"$Env:BOOTSTRAP_FUNC = $Env:BOOTSTRAP_ResourceGroup + \"Control\"\r\n",
|
||||
"$Env:BOOTSTRAP_STORAGE = $Env:BOOTSTRAP_ResourceGroup.ToLower() + \"storage\"\r\n",
|
||||
"$Env:BOOTSTRAP_BATCH = $Env:BOOTSTRAP_ResourceGroup.ToLower() + \"batch\"\r\n",
|
||||
"$Env:BOOTSTRAP_VNET = $Env:BOOTSTRAP_ResourceGroup + \"VNet\"\r\n",
|
||||
"$AdpSubscription = \"\" # Target Azure Subscription Name or ID to bootstrap data portability resources\r\n",
|
||||
"$AdpFunc = $AdpResourceGroup + \"Control\"\r\n",
|
||||
"$AdpStorage = $AdpResourceGroup.ToLower() + \"storage\"\r\n",
|
||||
"$AdpBatch = $AdpResourceGroup.ToLower() + \"batch\"\r\n",
|
||||
"$AdpVNET = $AdpResourceGroup + \"VNet\"\r\n",
|
||||
"\r\n",
|
||||
"# Bootstrapper URLs - Update with the recommended toolkit version and build\r\n",
|
||||
"$BaseToolkitUrl = \"https://hybridtoolkit.blob.core.windows.net/components\"\r\n",
|
||||
"$ReleaseVersion = \"0.13\"\r\n",
|
||||
"$BuildNumber = \"74938\"\r\n",
|
||||
"$Env:BOOTSTRAP_URL_FUNC = \"$BaseToolkitUrl/$ReleaseVersion/ADPControl-$BuildNumber.zip\"\r\n",
|
||||
"$Env:BOOTSTRAP_URL_WRAP = \"$BaseToolkitUrl/$ReleaseVersion/BatchWrapper-$BuildNumber.zip\"\r\n",
|
||||
"$AdpDownloadUrl = \"$BaseToolkitUrl/$ReleaseVersion/ADPControl-$BuildNumber.zip\"\r\n",
|
||||
"$AdpWrapperUrl = \"$BaseToolkitUrl/$ReleaseVersion/BatchWrapper-$BuildNumber.zip\"\r\n",
|
||||
"\r\n",
|
||||
"Write-Output \"Setting the Environment:\"\r\n",
|
||||
"Get-ChildItem Env: | Where-Object Name -Match \"BOOTSTRAP\""
|
||||
@@ -121,9 +121,9 @@
|
||||
" $subscriptions = az account list -o JSON | ConvertFrom-Json # getting subscriptions for the user to use in gridview\r\n",
|
||||
" }\r\n",
|
||||
"\r\n",
|
||||
" if(![string]::IsNullOrWhiteSpace($Env:BOOTSTRAP_Subscription)) #If there is a subscription specified by user in the variables section\r\n",
|
||||
" if(![string]::IsNullOrWhiteSpace($AdpSubscription)) #If there is a subscription specified by user in the variables section\r\n",
|
||||
" {\r\n",
|
||||
" $specified_Subscription= az account show --subscription $Env:BOOTSTRAP_Subscription -o json |ConvertFrom-Json \r\n",
|
||||
" $specified_Subscription= az account show --subscription $AdpSubscription -o json |ConvertFrom-Json \r\n",
|
||||
" if (!$specified_Subscription) #if specified subscription is not valid\r\n",
|
||||
" { \r\n",
|
||||
" $currentUser= az ad signed-in-user show --query \"{displayName:displayName,UPN:userPrincipalName}\" -o json|ConvertFrom-Json # get current logged in user infomration\r\n",
|
||||
@@ -139,8 +139,8 @@
|
||||
" $selectedSubscription = $subscriptions | Select-Object -Property Name, Id | Out-GridView -PassThru\r\n",
|
||||
" $SubscriptionId = $selectedSubscription.Id\r\n",
|
||||
" $Subscription = $selectedSubscription.Name \r\n",
|
||||
" $Env:BOOTSTRAP_Subscription = $subscription \r\n",
|
||||
" Write-Output \"Using subscription... '$Env:BOOTSTRAP_Subscription' ... '$SubscriptionId'\" \r\n",
|
||||
" $AdpSubscription = $subscription \r\n",
|
||||
" Write-Output \"Using subscription... '$AdpSubscription' ... '$SubscriptionId'\" \r\n",
|
||||
" } \r\n",
|
||||
"}\r\n",
|
||||
"\r\n",
|
||||
@@ -207,8 +207,8 @@
|
||||
" else { \r\n",
|
||||
" #VNet or defaut subnet not found under specified resource group. Create new VNet with default Subnet /Add default subnet to existing VNet\r\n",
|
||||
" Write-Output \"Creating new Virtual network with default Subnet ID ... \"\r\n",
|
||||
" $newVNet = az network vnet create --name \"$Env:BOOTSTRAP_VNET\" --resource-group $Env:BOOTSTRAP_ResourceGroup --subscription $Env:BOOTSTRAP_Subscription --subnet-name $SubNetName -o JSON |ConvertFrom-Json #vnet create/Update command: Bug: In this command, the output variable is not getting converted to PS objects.\r\n",
|
||||
" $newVNet = az network vnet subnet show -g $Env:BOOTSTRAP_ResourceGroup --vnet-name $Env:BOOTSTRAP_VNET -n $SubNetName --subscription $Env:BOOTSTRAP_Subscription -o JSON |ConvertFrom-Json # added this line due to above bug\r\n",
|
||||
" $newVNet = az network vnet create --name \"$AdpVNET\" --resource-group $AdpResourceGroup --subscription $AdpSubscription --subnet-name $SubNetName -o JSON |ConvertFrom-Json #vnet create/Update command: Bug: In this command, the output variable is not getting converted to PS objects.\r\n",
|
||||
" $newVNet = az network vnet subnet show -g $AdpResourceGroup --vnet-name $AdpVNET -n $SubNetName --subscription $AdpSubscription -o JSON |ConvertFrom-Json # added this line due to above bug\r\n",
|
||||
" Write-Output \"Created VNet with default Subnet - ID: '$($newVNet.id)'\"\r\n",
|
||||
" }\r\n",
|
||||
"}\r\n",
|
||||
@@ -508,7 +508,7 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"Bootstrap-AzResourceGroup -ResourceGroupName $Env:BOOTSTRAP_ResourceGroup -ResourceGroupLocation $Env:BOOTSTRAP_RG_REGION -Subscription $Env:BOOTSTRAP_Subscription"
|
||||
"Bootstrap-AzResourceGroup -ResourceGroupName $AdpResourceGroup -ResourceGroupLocation $AdpRegion -Subscription $AdpSubscription"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "9beb8d22-4560-4c7e-917b-5a3c0d58e1a2",
|
||||
@@ -533,7 +533,7 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"Bootstrap-AzVirtualNetwork -VNetName $Env:BOOTSTRAP_VNET -ResourceGroupName $Env:BOOTSTRAP_ResourceGroup -Subscription $Env:BOOTSTRAP_Subscription"
|
||||
"Bootstrap-AzVirtualNetwork -VNetName $AdpVNET -ResourceGroupName $AdpResourceGroup -Subscription $AdpSubscription"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "d014a6a6-57ff-4de7-8210-b3360bf34daa"
|
||||
@@ -555,7 +555,7 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"Bootstrap-AzStorageAccount -StorageAccountName $Env:BOOTSTRAP_STORAGE -ResourceGroupName $Env:BOOTSTRAP_ResourceGroup -Subscription $Env:BOOTSTRAP_Subscription"
|
||||
"Bootstrap-AzStorageAccount -StorageAccountName $AdpStorage -ResourceGroupName $AdpResourceGroup -Subscription $AdpSubscription"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "290498ee-3f31-4395-adab-a5fa93d28c80",
|
||||
@@ -580,8 +580,8 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"Bootstrap-AzFunctionApp -FunctionName $Env:BOOTSTRAP_FUNC -StorageAccountName $Env:BOOTSTRAP_STORAGE -FunctionAppPackageURL $Env:BOOTSTRAP_URL_FUNC `\r\n",
|
||||
" -ConsumptionPlanLocation $Env:BOOTSTRAP_RG_REGION -ResourceGroupName $Env:BOOTSTRAP_ResourceGroup -Subscription $Env:BOOTSTRAP_Subscription"
|
||||
"Bootstrap-AzFunctionApp -FunctionName $AdpFunc -StorageAccountName $AdpStorage -FunctionAppPackageURL $AdpDownloadUrl `\r\n",
|
||||
" -ConsumptionPlanLocation $AdpRegion -ResourceGroupName $AdpResourceGroup -Subscription $AdpSubscription"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "6fc2b5ec-c16f-4eb7-b2f9-c8c680d9a2df",
|
||||
@@ -605,8 +605,8 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"Bootstrap-AzBatchAccount -BatchAccountName $Env:BOOTSTRAP_BATCH -StorageAccountName $Env:BOOTSTRAP_STORAGE -BatchAccountLocation $Env:BOOTSTRAP_RG_REGION `\r\n",
|
||||
" -ApplicationPackageURL $Env:BOOTSTRAP_URL_WRAP -ResourceGroupName $Env:BOOTSTRAP_ResourceGroup -Subscription $Env:BOOTSTRAP_Subscription"
|
||||
"Bootstrap-AzBatchAccount -BatchAccountName $AdpBatch -StorageAccountName $AdpStorage -BatchAccountLocation $AdpRegion `\r\n",
|
||||
" -ApplicationPackageURL $AdpWrapperUrl -ResourceGroupName $AdpResourceGroup -Subscription $AdpSubscription"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "489733c4-1162-479b-82b4-b0c18954b25b",
|
||||
@@ -628,7 +628,7 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"Bootstrap-FuncRBAC -AzFunctionName $Env:BOOTSTRAP_FUNC -ResourceGroupName $Env:BOOTSTRAP_ResourceGroup -Subscription $Env:BOOTSTRAP_Subscription"
|
||||
"Bootstrap-FuncRBAC -AzFunctionName $AdpFunc -ResourceGroupName $AdpResourceGroup -Subscription $AdpSubscription"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "75882d3a-2004-4304-ab8f-e5146e14500c",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# Glossary
|
||||
[Home](readme.md)
|
||||
|
||||
A list of terms and their definitions can be found below
|
||||
|
||||
* **ADS** - *Azure Data Studio* is a desktop tool for managing Azure Data resources in the cloud, on-premises, or hybrid environments.
|
||||
@@ -31,4 +33,5 @@ A list of terms and their definitions can be found below
|
||||
* **SQL Assessment API** - evaluates a SQL instance configuration for best practices
|
||||
* **SQL Virtual Machine** - an IaaS Azure offer that provisions and manages virtual machine with SQL Server installed
|
||||
* **SQL Managed Instance** - a PaaS Azure offer for SQL Server that is ran on Azure infrastructure. Microsoft will manage the complexities of the infrastructure for the user
|
||||
* **SMO** - SQL Management Objects are "objects designed for programmatic management of Microsoft SQL Server" ([Microsoft](https://docs.microsoft.com/en-us/sql/relational-databases/server-management-objects-smo/overview-smo))
|
||||
* **VPN** - a *virtual private network* is a collection of computing resources that organizes and extends a private network configuration over the public Internet, normally using some kind of encryption for security and privacy.
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"name": "python3",
|
||||
"display_name": "Python 3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.6.6",
|
||||
"mimetype": "text/x-python",
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"pygments_lexer": "ipython3",
|
||||
"nbconvert_exporter": "python",
|
||||
"file_extension": ".py"
|
||||
}
|
||||
},
|
||||
"nbformat_minor": 2,
|
||||
"nbformat": 4,
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"Add Azure Passive Secondary Replica\n",
|
||||
"============================================\n",
|
||||
"\n",
|
||||
"Description\n",
|
||||
"-----------\n",
|
||||
"\n",
|
||||
"Notebook to walkthrough extending an on-premises Availability Group with an Azure Passive Secondary Replica."
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "a7c75090-5d5f-4a1b-8712-461a0921f4ad"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
# High Availability and Disaster Recovery
|
||||
|
||||
[Home](../readme.md)
|
||||
|
||||
**Coming soon**: Notebooks to help with HADR tasks in a Hybrid Cloud environment.
|
||||
Notebooks to help with HADR tasks in a Hybrid Cloud environment.
|
||||
|
||||
## Notebooks in this Chapter
|
||||
- [Backup Database to Blob Storage](backup-to-blob.ipynb)
|
||||
|
||||
- [Add Azure Passive Secondary Replica](add-passive-secondary.ipynb)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# Networking
|
||||
[Home](../readme.md)
|
||||
|
||||
This chapter contains notebooks to configure and make a secure network connection in an Azure hybrid cloud environment.
|
||||
|
||||
<img width="50%" src="https://docs.microsoft.com/en-us/azure/vpn-gateway/media/point-to-site-about/p2s.png">
|
||||
|
||||
## Notebooks in this Chapter
|
||||
- [Download VPN Client Certificate](download-VpnClient.ipynb) - Used to install certificates that encrypt communication between on-site and Azure services
|
||||
|
||||
- [Create Point-to-Site VPN](p2svnet-creation.ipynb) - Enables secure **Point-to-Site** (P2S) communication between a virtual private network in Azure and local resources. P2S is used by individuals and small groups for remote connectivity. A Point-to-Site (P2S) VPN gateway connection lets you create a secure connection to your VPN from an individual client computer. A P2S connection is established by starting it from the client computer. This solution is useful for telecommuters who want to connect to Azure VNets from a remote location, such as from home or a conference. P2S VPN is also a useful solution to use instead of S2S VPN when you have only a few clients that need to connect to a virtual network.
|
||||
|
||||
- [Create Site-to-Site VPN](s2svnet-creation.ipynb) - **Site-to-site** (S2S) is normally used by organizations that want greater control between on-premise and cloud resources using a VPN gateway. A S2S VPN gateway connection is used to connect your on-premises network to an Azure virtual network over an IPsec/IKE (IKEv1 or IKEv2) VPN tunnel. This type of connection requires a VPN device located on-premises that has an externally facing public IP address assigned to it. For more information about VPN gateways, see [About VPN gateway](https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-about-vpngateways) and [Create and manage S2S VPN connections using PowerShell](https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-tutorial-vpnconnection-powershell "https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-tutorial-vpnconnection-powershell"). **NOTE:** *May require the help of a Network Administrator or similar role to setup a secure Gateway*.
|
||||
|
||||
This chapter contains notebooks to configure and make a secure network connection in an Azure hybrid cloud environment.
|
||||
|
||||
<img width="50%" src="https://docs.microsoft.com/en-us/azure/vpn-gateway/media/point-to-site-about/p2s.png">
|
||||
- [Create Site-to-Site VPN](s2svnet-creation.ipynb) - **Site-to-site** (S2S) is normally used by organizations that want greater control between on-premise and cloud resources using a VPN gateway. A S2S VPN gateway connection is used to connect your on-premises network to an Azure virtual network over an IPsec/IKE (IKEv1 or IKEv2) VPN tunnel. This type of connection requires a VPN device located on-premises that has an externally facing public IP address assigned to it. For more information about VPN gateways, see [About VPN gateway](https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-about-vpngateways) and [Create and manage S2S VPN connections using PowerShell](https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-tutorial-vpnconnection-powershell "https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-tutorial-vpnconnection-powershell"). **NOTE:** *May require the help of a Network Administrator or similar role to setup a secure Gateway*.
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"name": "python3",
|
||||
"display_name": "Python 3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.7.8",
|
||||
"mimetype": "text/x-python",
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"pygments_lexer": "ipython3",
|
||||
"nbconvert_exporter": "python",
|
||||
"file_extension": ".py"
|
||||
}
|
||||
},
|
||||
"nbformat_minor": 2,
|
||||
"nbformat": 4,
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"Migrate a Database to a Azure SQL Managed Instance\n",
|
||||
"=============================================\n",
|
||||
"\n",
|
||||
"Description\n",
|
||||
"-----\n",
|
||||
"\n",
|
||||
"Copies the database from an on-premises SQL instance to an Azure SQL Managed Instance."
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "5353c044-9920-478b-b1f8-e98119b73a21"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -51,15 +51,14 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"$sourceServerName = 'sqltools2016-3'\r\n",
|
||||
"$sourceLogin = 'migtest'\r\n",
|
||||
"$sourceServerName = '<server_name>'\r\n",
|
||||
"$sourceLogin = '<user_name>'\r\n",
|
||||
"\r\n",
|
||||
"## TEMP - REMOVE BEFORE PUSHING CHANGES\r\n",
|
||||
"$env:SQLMIG_SourcePassword = 'Yukon900'\r\n",
|
||||
"$env:SQLMIG_SourcePassword = '<user_pass>'\r\n",
|
||||
"\r\n",
|
||||
"## PowerShell Environment \r\n",
|
||||
"$sourceLoginPassword = ConvertTo-SecureString $env:SQLMIG_SourcePassword -AsPlaintext -Force\r\n",
|
||||
"$sourceCredential = New-Object System.Management.Automation.PSCredential ('migtest', $sourceLoginPassword)\r\n",
|
||||
"$sourceCredential = New-Object System.Management.Automation.PSCredential ('<user_name>', $sourceLoginPassword)\r\n",
|
||||
"$sourceTest = Test-DbaConnection -SqlInstance $sourceServerName -SqlCredential $sourceCredential\r\n",
|
||||
"$sourceTest\r\n",
|
||||
"$sourceConnection = Connect-DbaInstance -SqlInstance $sourceServerName -SqlCredential $sourceCredential"
|
||||
@@ -98,11 +97,11 @@
|
||||
"$targetLogin = 'cloudsa'\r\n",
|
||||
"\r\n",
|
||||
"## TEMP - REMOVE BEFORE PUSHING CHANGES\r\n",
|
||||
"$env:SQLMIG_TargetPassword = 'Yukon900Yukon900'\r\n",
|
||||
"$env:SQLMIG_TargetPassword = '<user_pass>'\r\n",
|
||||
"\r\n",
|
||||
"## PowerShell Environment \r\n",
|
||||
"$targetLoginPassword = ConvertTo-SecureString $env:SQLMIG_TargetPassword -AsPlaintext -Force\r\n",
|
||||
"$targetCredential = New-Object System.Management.Automation.PSCredential ('migtest', $targetLoginPassword)\r\n",
|
||||
"$targetCredential = New-Object System.Management.Automation.PSCredential ('<user_name>', $targetLoginPassword)\r\n",
|
||||
"$targetTest = Test-DbaConnection -SqlInstance $targetServerName -SqlCredential $targetCredential\r\n",
|
||||
"$targetTest\r\n",
|
||||
"$targetConnection = Connect-DbaInstance -SqlInstance $targetServerName -SqlCredential $targetCredential"
|
||||
@@ -268,4 +267,4 @@
|
||||
"execution_count": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"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": [
|
||||
"Migrate SQL Server Instance to Azure SQL Managed Instance\n",
|
||||
"=============================================\n",
|
||||
"\n",
|
||||
"Description\n",
|
||||
"-----\n",
|
||||
"\n",
|
||||
"clone the configuration and data of a sql instance into a managed instance\n",
|
||||
""
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "43600853-57b3-4e60-a2a9-a28fb82af386"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -8,8 +8,5 @@ This chapter contains a set of notebooks useful for doing offline migration of d
|
||||
|
||||
- [Migrate Database to Azure SQL VM](db-to-VM.ipynb)
|
||||
|
||||
- [Migrate Instance to Azure SQL MI](instance-to-MI.ipynb)
|
||||
|
||||
- [Migrate Database to Azure SQL MI](db-to-MI.ipynb)
|
||||
|
||||
- [Migrate Database to Azure SQL DB](db-to-SQLDB.ipynb)
|
||||
|
||||
|
||||
@@ -126,7 +126,13 @@
|
||||
"SQL Assessment API is part of the SQL Server Management Objects (SMO) and can be used with the SQL Server PowerShell module. Because installing the modules may require a local Administrator account's permission, it cannot be done automatically with this Notebook. The **Assessments** Notebooks require the following:\n",
|
||||
"\n",
|
||||
"- [Install SMO](https://docs.microsoft.com/en-us/sql/relational-databases/server-management-objects-smo/installing-smo?view=sql-server-ver15)\n",
|
||||
"- [Install SQL Server PowerShell module](https://docs.microsoft.com/en-us/sql/powershell/download-sql-server-ps-module?view=sql-server-ver15)"
|
||||
"- [Install SQL Server PowerShell module](https://docs.microsoft.com/en-us/sql/powershell/download-sql-server-ps-module?view=sql-server-ver15)\n",
|
||||
"\n",
|
||||
"## Compatibility Assessment Tool - Data Migration Assistant\n",
|
||||
"\n",
|
||||
"The Compatibility Assessment Notebook requires the Data Migration Assistant tool to be installed in order to execute. The installation link would be [Data Migration Assistant download](https://www.microsoft.com/en-us/download/confirmation.aspx?id=53595)\n",
|
||||
"\n",
|
||||
"With version 2.1 and above, when installation of Data Migration Assistant is successful, it will install dmacmd.exe in _%ProgramFiles%\\\\Microsoft Data Migration Assistant_ folder."
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "1b49a7e5-a773-4104-8f88-bd2ea3c806a3"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# Azure SQL Provisioning
|
||||
# Provisioning
|
||||
[Home](../readme.md)
|
||||
|
||||
This chapter contains Notebooks that help provision new Azure SQL resources that can be used as migration targets for existing on-premises SQL instances and databases. Use alongside the planning notebooks to use existing resources as the basis for the best type of resource to create and how it should be configured. You can use the notebooks and configure the settings manually or provide a provisioning plan created by the [Create Provisioning Plan](../provisioning/provisioning-plan.ipynb) notebook.
|
||||
|
||||
## Notebooks in this Chapter
|
||||
- [Create Azure SQL Virtual Machine](create-sqlvm.ipynb) - SQL Server on Azure Virtual Machines enables to use full versions of SQL Server in the cloud without having to manage any on-premises hardware. The virtual machine image gallery allows to create a SQL Server VM with the right version, edition, and operating system
|
||||
- [Create Azure SQL Managed Instance](create-sqlmi.ipynb) - Azure SQL Managed Instance is the intelligent, scalable, cloud database service that combines the broadest SQL Server engine compatibility with all the benefits of a fully managed and evergreen platform as a service. An instance is a copy of the sqlservr.exe executable that runs as an operating system service
|
||||
- [Create Azure SQL Database](create-sqldb.ipynb) - Azure SQL Database is Microsoft's fully managed cloud relational database service in Microsoft Azure. It shares the same code base as traditional SQL Servers but with Microsoft's Cloud first strategy the newest features of SQL Server are actually released to Azure SQL Database first. Use this notebook when a need is systematic collection of data that stores data in tables
|
||||
|
||||
This chapter contains Notebooks that help provision new Azure SQL resources that can be used as migration targets for existing on-premises SQL instances and databases. Use alongside the planning notebooks to use existing resources as the basis for the best type of resource to create and how it should be configured. You can use the notebooks and configure the settings manually or provide a provisioning plan created by the [Create Provisioning Plan](../provisioning/provisioning-plan.ipynb) notebook.
|
||||
- [Create Azure SQL Database](create-sqldb.ipynb) - Azure SQL Database is Microsoft's fully managed cloud relational database service in Microsoft Azure. It shares the same code base as traditional SQL Servers but with Microsoft's Cloud first strategy the newest features of SQL Server are actually released to Azure SQL Database first. Use this notebook when a need is systematic collection of data that stores data in tables
|
||||
@@ -1,28 +1,35 @@
|
||||
# Azure SQL Hybrid Cloud Toolkit
|
||||
# Welcome to the Azure SQL Hybrid Cloud Toolkit!
|
||||
## Chapters
|
||||
* [Prerequisites and Initial Setup](prereqs.ipynb) - Notebook installation of required modules.
|
||||
|
||||
The **Azure SQL Hybrid Cloud Toolkit** is a [Jupyter Book](https://jupyterbook.org/intro.html) extension of [Azure Data Studio](https://docs.microsoft.com/en-us/sql/azure-data-studio/download-azure-data-studio) (ADS) designed to help [Azure SQL Database](https://azure.microsoft.com/en-us/services/sql-database/) and ADS users deploy, migrate and configure for a hybrid cloud environment. The toolkit was designed with and intended to be executed within ADS. This is to ensure the best possible experience.
|
||||
* [Assessments](Assessments/readme.md) - Notebooks that contain examples to determine whether a given database or SQL Server instance is ready to migrate by utilizing SQL Assessments. SQL instances are scanned based on a "best practices" set of rules.
|
||||
|
||||
* [Networking](networking/readme.md) - Setup secure Point-to-Site (P2S) or Site-to-Site (S2S) network connectivity to Microsoft Azure using a Virtual Private Network (VPN). This notebook serves as a building block for other notebooks as communicating securely between on-premise and Azure is essential for many tasks.
|
||||
|
||||
* [Provisioning](provisioning/readme.md) - Creating and communicating with SQL Resources in Microsoft Azure. Includes common tasks such as creating SQL Virtual Machines or SQL Managed Instances in the cloud.
|
||||
|
||||
* [Data Portability](data-portability/readme.md) - Install a custom Azure function to facilitate importing and exporting cloud resources. The solution uses parallel tasks in Azure Batch to perform data storage work. Azure Batch is a process that runs large-scale parallel and high-performance computing jobs efficiently in Azure.
|
||||
|
||||
* [High Availability and Disaster Recovery](hadr/readme.md) - Notebooks to leverage Azure SQL for business continuity in a hybrid cloud environment.
|
||||
|
||||
* [Offline Migration](offline-migration/readme.md) - Notebooks to perform various migrations.
|
||||
|
||||
* [Glossary](glossary.md) - set of defined terms.
|
||||
|
||||
* [Appendices](appendices.md) - misc info.
|
||||
|
||||
## About
|
||||
|
||||
The **Azure SQL Hybrid Cloud Toolkit** is a [Jupyter Book](https://jupyterbook.org/intro.html) extension of [Azure Data Studio](https://docs.microsoft.com/en-us/sql/azure-data-studio/download-azure-data-studio) (ADS) designed to help [Azure SQL Database](https://azure.microsoft.com/en-us/services/sql-database/) and ADS users deploy, migrate and configure for a hybrid cloud environment. The toolkit was designed with and intended to be executed within ADS. This is to ensure the best possible user experience for those without vast knowledge of Azure services while adhering closely to the software _best practices_ standards required by experienced cloud users.
|
||||
|
||||
## Goals and Methodology
|
||||
The toolkit better positions a customer with regards to planning, migrating, and thriving in a hybrid cloud environment by:
|
||||
|
||||
* Providing SQL Azure users with reliable free software and content that is well-written and executable
|
||||
* Providing SQL'zure users with reliable free software and content that is well-written and executable
|
||||
* Greatly simplifying the integration of Azure Data services into an existing environment
|
||||
* Positioning Azure to be the natural cloud services choice with a low-friction experience
|
||||
* Notebooks are executable by a normal user (unless otherwise specificed) on minimal hardware
|
||||
* Most notebooks require some configuration. If so, the proper configurations should be clearly located towards the top of the notebook or cell, whichever is most appropriate
|
||||
* Modify the cells to meet the desired requirements
|
||||
* By design, Notebooks are written to be executed from top-to-bottom. Therefore, each notebook has a specific task to perform and should focus only on that task. It may contain several cells to execute but it will adhere to the one-task per notebook paradigm
|
||||
|
||||
**NOTE:** Executing notebooks could potentially create new Azure Resources which may incur charges to the Azure Subscription. Make sure the repercussions of executing any cells are understood.
|
||||
|
||||
## Prerequisites and Initial Setup
|
||||
The notebooks may leverage various modules from Python or Microsoft PowerShell and the OSS community. To execute the notebooks in this toolkit, start with the [Prerequisites and Initial Setup Notebook](Prerequisites/prereqs.ipynb) where all prerequisite modules will be checked and installed if not found in the execution environment.
|
||||
|
||||
## Chapters
|
||||
The toolkit has chapters on network configuration, on-premise SQL Server assessment, resource provisioning, and Azure migration. See below:
|
||||
* [Networking](networking/readme.md) - Setup secure Point-to-Site (P2S) or Site-to-Site (S2S) network connectivity to Microsoft Azure using a Virtual Private Network (VPN). This notebook serves as a building block for other notebooks as communicating securely between on-premise and Azure is essential for many tasks
|
||||
* [Assessments](Assessments/readme.md) - Notebooks that contain examples to determine whether a given database or SQL Server instance is ready to migrate by utilizing SQL Assessments. SQL instances are scanned based on a "best practices" set of rules.
|
||||
* [Provisioning](provisioning/readme.md) - Creating and communicating with SQL Resources in Microsoft Azure. Includes common tasks such as creating SQL Virtual Machines or SQL Managed Instances in the cloud
|
||||
* [Data Portability](data-portability/readme.md) - Install a custom Azure function to facilitate importing and exporting cloud resources. The solution uses parallel tasks in Azure Batch to perform data storage work. Azure Batch is a process that runs large-scale parallel and high-performance computing jobs efficiently in Azure.
|
||||
* [High Availability and Disaster Recovery](hadr/readme.md) - Notebooks to leverage Azure SQL for business continuity in a hybrid cloud environment
|
||||
* [Offline Migration](offline-migration/readme.md) - Notebooks to perform various migrations
|
||||
**NOTE:** Executing notebooks could potentially create new Azure Resources which may incur charges to the Azure Subscription. Make sure the repercussions of executing any cells are understood.
|
||||
@@ -4,14 +4,26 @@
|
||||
"description": "%description%",
|
||||
"version": "0.1.0",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
||||
"icon": "images/extension.png",
|
||||
"aiKey": "AIF-37eefaf0-8022-4671-a3fb-64752724682e",
|
||||
"engines": {
|
||||
"vscode": "*",
|
||||
"azdata": "*"
|
||||
},
|
||||
"main": "./out/main",
|
||||
"activationEvents": [
|
||||
"*"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Microsoft/azuredatastudio.git"
|
||||
},
|
||||
"main": "./out/main",
|
||||
"extensionDependencies": [
|
||||
"Microsoft.mssql",
|
||||
"Microsoft.notebook"
|
||||
],
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"displayName": "Azure SQL Hybrid Cloud Toolkit Jupyter Book Extension",
|
||||
"displayName": "Azure SQL Hybrid Cloud Toolkit",
|
||||
"description": "Opens up Azure SQL Hybrid Cloud Toolkit Jupyter Book",
|
||||
"title.openJupyterBook": "Open Azure SQL Hybrid Cloud Toolkit Jupyter Book",
|
||||
"title.cloudHybridBooks": "Azure SQL Hybrid Cloud Toolkit",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user