* Revert "Remove all Big Data Cluster features (#21369)"
This reverts commit e2327c393a.
* Bump STS
9
extensions/big-data-cluster/.eslintrc.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"parserOptions": {
|
||||
"project": "./extensions/big-data-cluster/tsconfig.json"
|
||||
},
|
||||
"rules": {
|
||||
// Disabled until the issues can be fixed
|
||||
"@typescript-eslint/explicit-function-return-type": ["off"]
|
||||
}
|
||||
}
|
||||
1
extensions/big-data-cluster/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.vsix
|
||||
14
extensions/big-data-cluster/.vscodeignore
Normal file
@@ -0,0 +1,14 @@
|
||||
.gitignore
|
||||
instructions.txt
|
||||
src/**
|
||||
out/**
|
||||
extension.webpack.config.js
|
||||
tsconfig.json
|
||||
yarn.lock
|
||||
|
||||
node_modules
|
||||
!node_modules/@microsoft/ads-kerberos/package.json
|
||||
!node_modules/@microsoft/ads-kerberos/LICENSE
|
||||
!node_modules/@microsoft/ads-kerberos/lib
|
||||
!node_modules/@microsoft/ads-kerberos/index.js
|
||||
!node_modules/@microsoft/ads-kerberos/build/Release/kerberos.node
|
||||
17
extensions/big-data-cluster/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Microsoft SQL Server Big Data Cluster Extension for Azure Data Studio
|
||||
|
||||
Welcome to Microsoft SQL Server Big Data Cluster Extension for Azure Data Studio!
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||
|
||||
## Privacy Statement
|
||||
|
||||
The [Microsoft Enterprise and Developer Privacy Statement](https://privacy.microsoft.com/privacystatement) describes the privacy statement of this software.
|
||||
|
||||
## License
|
||||
|
||||
Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
Licensed under the [Source EULA](https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt).
|
||||
20
extensions/big-data-cluster/extension.webpack.config.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
|
||||
'use strict';
|
||||
|
||||
const withDefaults = require('../shared.webpack.config');
|
||||
|
||||
module.exports = withDefaults({
|
||||
context: __dirname,
|
||||
entry: {
|
||||
extension: './src/extension.ts'
|
||||
},
|
||||
externals: {
|
||||
'@microsoft/ads-kerberos': 'commonjs @microsoft/ads-kerberos'
|
||||
}
|
||||
});
|
||||
BIN
extensions/big-data-cluster/images/extension.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
38
extensions/big-data-cluster/images/sql_bdc.svg
Normal file
@@ -0,0 +1,38 @@
|
||||
<svg id="a8b9457a-0277-4e4a-af7b-2a35817c767b" data-name="icon" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="18" height="18" viewBox="0 0 18 18">
|
||||
<defs>
|
||||
<linearGradient id="acf91304-a8c9-4c1f-b6a5-36f9b8128409" x1="2.59" y1="-314.16" x2="15.41" y2="-314.16" gradientTransform="matrix(1, 0, 0, -1, 0, -304)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#005ba1"/>
|
||||
<stop offset="0.15" stop-color="#0078d4"/>
|
||||
<stop offset="0.242" stop-color="#1c84dc"/>
|
||||
<stop offset="0.414" stop-color="#4c98ea"/>
|
||||
<stop offset="0.5" stop-color="#5ea0ef"/>
|
||||
<stop offset="0.586" stop-color="#4c98ea"/>
|
||||
<stop offset="0.758" stop-color="#1c84dc"/>
|
||||
<stop offset="0.85" stop-color="#0078d4"/>
|
||||
<stop offset="1" stop-color="#005ba1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="ea6a9244-85f4-45fd-be4c-117e3f7d4184" x1="93.192" y1="-20.889" x2="94.554" y2="-19.014" gradientTransform="translate(-76.373 -31.311) rotate(36)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.19" stop-color="#9cebff"/>
|
||||
<stop offset="0.91" stop-color="#50e6ff"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="b24253bf-9577-4f4b-8c38-5fd96995c4b7" x1="-75.25" y1="-20.085" x2="-76.612" y2="-18.21" gradientTransform="translate(79.811 -20.731) rotate(-36)" xlink:href="#ea6a9244-85f4-45fd-be4c-117e3f7d4184"/>
|
||||
<linearGradient id="ba4dd4c3-07ad-4d29-9073-416b60bf6972" x1="9.026" y1="-279.302" x2="9.026" y2="-281.619" gradientTransform="translate(18 -266.363) rotate(180)" xlink:href="#ea6a9244-85f4-45fd-be4c-117e3f7d4184"/>
|
||||
<linearGradient id="f7bc97d7-e820-4bb7-bdc1-4286127d66c0" x1="-127.278" y1="-180.991" x2="-129.482" y2="-181.707" gradientTransform="translate(138.136 -166.001) rotate(-108)" xlink:href="#ea6a9244-85f4-45fd-be4c-117e3f7d4184"/>
|
||||
<linearGradient id="b0b2087b-b12e-437b-a3ec-e7ad6bbea89b" x1="145.294" y1="-180.259" x2="147.498" y2="-180.975" gradientTransform="translate(-114.574 -183.12) rotate(108)" xlink:href="#ea6a9244-85f4-45fd-be4c-117e3f7d4184"/>
|
||||
</defs>
|
||||
<g>
|
||||
<path d="M9,5.14c-3.54,0-6.41-1-6.41-2.32V15.18c0,1.27,2.82,2.3,6.32,2.32H9c3.54,0,6.41-1,6.41-2.32V2.82C15.41,4.11,12.54,5.14,9,5.14Z" fill="url(#acf91304-a8c9-4c1f-b6a5-36f9b8128409)"/>
|
||||
<path d="M15.41,2.82c0,1.29-2.87,2.32-6.41,2.32s-6.41-1-6.41-2.32S5.46.5,9,.5s6.41,1,6.41,2.32" fill="#e8e8e8"/>
|
||||
<path d="M13.92,2.63c0,.82-2.21,1.48-4.92,1.48S4.08,3.45,4.08,2.63,6.29,1.16,9,1.16s4.92.66,4.92,1.47" fill="#b3b3b3"/>
|
||||
<path d="M9,3a11.541,11.541,0,0,0-3.89.57A11.428,11.428,0,0,0,9,4.11a11.149,11.149,0,0,0,3.89-.58A11.839,11.839,0,0,0,9,3Z" fill="#767676"/>
|
||||
</g>
|
||||
<polyline points="11.299 7.724 10.806 8.41 8.974 10.953 7.143 8.41" fill="none" stroke="#f2f2f2" stroke-miterlimit="10" stroke-width="0.75"/>
|
||||
<circle cx="11.299" cy="7.724" r="1.158" fill="url(#ea6a9244-85f4-45fd-be4c-117e3f7d4184)"/>
|
||||
<circle cx="7.126" cy="8.41" r="1.158" fill="url(#b24253bf-9577-4f4b-8c38-5fd96995c4b7)"/>
|
||||
<line x1="8.974" y1="10.953" x2="8.974" y2="14.097" fill="none" stroke="#f2f2f2" stroke-miterlimit="10" stroke-width="0.75"/>
|
||||
<polyline points="8.974 10.953 5.984 11.925 5.335 12.136" fill="none" stroke="#f2f2f2" stroke-miterlimit="10" stroke-width="0.75"/>
|
||||
<line x1="11.964" y1="11.925" x2="8.974" y2="10.953" fill="none" stroke="#f2f2f2" stroke-miterlimit="10" stroke-width="0.75"/>
|
||||
<circle cx="8.974" cy="14.097" r="1.158" fill="url(#ba4dd4c3-07ad-4d29-9073-416b60bf6972)"/>
|
||||
<circle cx="5.335" cy="12.136" r="1.158" fill="url(#f7bc97d7-e820-4bb7-bdc1-4286127d66c0)"/>
|
||||
<circle cx="11.964" cy="11.925" r="1.158" fill="url(#b0b2087b-b12e-437b-a3ec-e7ad6bbea89b)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
25
extensions/big-data-cluster/instructions.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
How to update the Swagger-generated API to contact the controller
|
||||
|
||||
## BdcRouter API:
|
||||
1. You need to get the API specification. Long-term you should be able to get from the server,
|
||||
but for now go to the internal repository and find the checked in SwaggerClient.yaml there.
|
||||
|
||||
2. Copy the content from there, and add into https://editor.swagger.io/
|
||||
3. Choose Generate Client, and choose Typescript-Node as the client to generate
|
||||
4. This will download a zip file. Open it and copy contents of api.ts
|
||||
5. Copy this content to apiGenerated.ts
|
||||
- keep the copyright header and everything above the let defaultBasePath = xyz line,
|
||||
- Override the rest of the file
|
||||
6. Format the apiGenerated.ts file so it passes gulp hygiene
|
||||
|
||||
## TokenRouter and other APIs:
|
||||
1. Get the API spec. This is available from a cluster at the address https://<ip>:30080/docs/swagger.json, where <ip> is the controller IP address.
|
||||
2. Copy the content from there, and add convert from OpenApi 3.0 to Swagger 2.0 so we can use the Typescript-Node client generated by https://editor.swagger.io/.
|
||||
Various converter tools are online. Alternatively, we might be able to use a different generator that has this client type (e.g. npm package @openapitools/openapi-generator-cli) but some require Java install.
|
||||
3. Copy the converted Swagger 2.0 spec into https://editor.swagger.io/
|
||||
4. Choose Generate Client, and choose Typescript-Node as the client to generate
|
||||
5. This will download a zip file. Open it and copy contents of api.ts
|
||||
6. Copy this content to tokenApiGenerated.ts
|
||||
- keep the copyright header and everything above the let defaultBasePath = xyz line,
|
||||
- Override the rest of the file
|
||||
7. Format the tokenApiGenerated.ts file so it passes gulp hygiene
|
||||
@@ -0,0 +1,430 @@
|
||||
{
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"name": "python3",
|
||||
"display_name": "Python 3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.6.6",
|
||||
"mimetype": "text/x-python",
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"pygments_lexer": "ipython3",
|
||||
"nbconvert_exporter": "python",
|
||||
"file_extension": ".py"
|
||||
}
|
||||
},
|
||||
"nbformat_minor": 2,
|
||||
"nbformat": 4,
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"\n",
|
||||
" \n",
|
||||
"## Create Azure Kubernetes Service cluster and deploy SQL Server 2019 Big Data Cluster\n",
|
||||
" \n",
|
||||
"This notebook walks through the process of creating a new Azure Kubernetes Service cluster first, and then deploys a <a href=\"https://docs.microsoft.com/sql/big-data-cluster/big-data-cluster-overview?view=sqlallproducts-allversions\">SQL Server 2019 Big Data Cluster</a> on the newly created AKS cluster.\n",
|
||||
" \n",
|
||||
"* Follow the instructions in the **Prerequisites** cell to install the tools if not already installed.\n",
|
||||
"* The **Required information** will check and prompt you for password if it is not set in the environment variable. The password will be used to access the cluster controller, SQL Server, and Knox.\n",
|
||||
"\n",
|
||||
"<span style=\"color:red\"><font size=\"3\">Please press the \"Run all\" button to run the notebook</font></span>"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "4f6bc3bc-3592-420a-b534-384011189005"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Prerequisites**\n",
|
||||
"Ensure the following tools are installed and added to PATH before proceeding.\n",
|
||||
"\n",
|
||||
"|Tools|Description|Installation|\n",
|
||||
"|---|---|---|\n",
|
||||
"|Azure CLI |Command-line tool for managing Azure services. Used to create AKS cluster | [Installation](https://docs.microsoft.com/cli/azure/install-azure-cli?view=azure-cli-latest) |\n",
|
||||
"|kubectl | Command-line tool for monitoring the underlying Kubernetes cluster | [Installation](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl-binary-using-native-package-management) |\n",
|
||||
"|azdata | Command-line tool for installing and managing a Big Data Cluster |[Installation](https://docs.microsoft.com/en-us/sql/big-data-cluster/deploy-install-azdata?view=sqlallproducts-allversions) |"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "d949980e-ad3f-4d02-ae84-7e4fbb19a087"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Setup**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "a56d3413-a730-4997-b5c2-c8abd972757e"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"import pandas,sys,os,json,html,getpass,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",
|
||||
"def run_command(command):\n",
|
||||
" print(\"Executing: \" + command)\n",
|
||||
" !{command}\n",
|
||||
" if _exit_code != 0:\n",
|
||||
" sys.exit(f'Command execution failed with exit code: {str(_exit_code)}.\\n\\t{command}\\n')\n",
|
||||
" print(f'Successfully executed: {command}')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "326645cf-022a-47f2-8aff-37de71da8955",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Set variables**\n",
|
||||
"Generated by Azure Data Studio using the values collected in the Deploy Big Data Cluster wizard"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "8716915b-1439-431b-ab0a-0221ef94cb7f"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Check dependencies**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "db8b1e21-eb2c-4c35-b973-bc4ef38bb1d0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"run_command('kubectl version --client=true')\n",
|
||||
"run_command('azdata --version')\n",
|
||||
"run_command('az --version')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "9361deaf-28b1-4d02-912d-2011cae97e8a",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Required information**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "720c200c-322a-49dd-9aa3-8bf7946aa251"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"invoked_by_wizard = \"AZDATA_NB_VAR_BDC_ADMIN_PASSWORD\" in os.environ\n",
|
||||
"if invoked_by_wizard:\n",
|
||||
" mssql_password = os.environ[\"AZDATA_NB_VAR_BDC_ADMIN_PASSWORD\"]\n",
|
||||
"else:\n",
|
||||
" mssql_password = getpass.getpass(prompt = 'SQL Server 2019 Big Data Cluster controller password')\n",
|
||||
" if mssql_password == \"\":\n",
|
||||
" sys.exit(f'Password is required.')\n",
|
||||
" confirm_password = getpass.getpass(prompt = 'Confirm password')\n",
|
||||
" if mssql_password != confirm_password:\n",
|
||||
" sys.exit(f'Passwords do not match.')\n",
|
||||
"print('You can also use the controller password to access Knox and SQL Server.')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "17e5d087-7128-4d02-8c16-fe1ddee675e5",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Login to Azure**\n",
|
||||
"\n",
|
||||
"This will open a web browser window to enable credentials to be entered. If this cells is hanging forever, it might be because your Web browser windows is waiting for you to enter your Azure credentials!\n",
|
||||
""
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "baddf2d9-93ee-4c42-aaf1-b42116bb1912"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"run_command(f'az login')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "8f1404a6-216d-49fb-b6ad-81beeea50083",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"\n",
|
||||
"### **Set active Azure subscription**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "230dc0f1-bf6e-474a-bfaa-aae6f8aad12e"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"if azure_subscription_id != \"\":\n",
|
||||
" run_command(f'az account set --subscription {azure_subscription_id}')\n",
|
||||
"else:\n",
|
||||
" print('Using the default Azure subscription', {azure_subscription_id})\n",
|
||||
"run_command(f'az account show')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "ab230931-2e99-483b-a229-3847684a8c1c",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Create Azure resource group**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "d51db914-f484-489f-990d-72edb3065068"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"run_command(f'az group create --name {azure_resource_group} --location {azure_region}')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "7c53eb23-c327-41bf-8936-bd34a02ebdd5",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Create AKS cluster**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "818eb705-71e2-4013-8420-44886a5468b2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"run_command(f'az aks create --name {aks_cluster_name} --resource-group {azure_resource_group} --generate-ssh-keys --node-vm-size {azure_vm_size} --node-count {azure_vm_count}')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "3cea1da0-0c18-4030-a5aa-79bc98a5a14d",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Set the new AKS cluster as current context**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "5ade8453-5e71-478f-b6b6-83c55626243d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"run_command(f'az aks get-credentials --resource-group {azure_resource_group} --name {aks_cluster_name} --admin --overwrite-existing')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "9ccb9adf-1cf6-4dcb-8bd9-7ae9a85c2437",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Create deployment configuration files**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "57eb69fb-c68f-4ba8-818d-ffbaa0bc7aec"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"mssql_target_profile = 'ads-bdc-custom-profile'\n",
|
||||
"if not os.path.exists(mssql_target_profile):\n",
|
||||
" os.mkdir(mssql_target_profile)\n",
|
||||
"bdcJsonObj = json.loads(bdc_json)\n",
|
||||
"controlJsonObj = json.loads(control_json)\n",
|
||||
"bdcJsonFile = open(f'{mssql_target_profile}/bdc.json', 'w')\n",
|
||||
"bdcJsonFile.write(json.dumps(bdcJsonObj, indent = 4))\n",
|
||||
"bdcJsonFile.close()\n",
|
||||
"controlJsonFile = open(f'{mssql_target_profile}/control.json', 'w')\n",
|
||||
"controlJsonFile.write(json.dumps(controlJsonObj, indent = 4))\n",
|
||||
"controlJsonFile.close()\n",
|
||||
"print(f'Created deployment configuration folder: {mssql_target_profile}')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "3fd73c04-8a79-4d08-9049-1dad30265558",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Create SQL Server 2019 Big Data Cluster**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "6e82fad8-0fd0-4952-87ce-3fea1edd98cb"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"print (f'Creating SQL Server 2019 Big Data Cluster: {mssql_cluster_name} using configuration {mssql_target_profile}')\n",
|
||||
"os.environ[\"ACCEPT_EULA\"] = 'yes'\n",
|
||||
"os.environ[\"AZDATA_USERNAME\"] = mssql_username\n",
|
||||
"os.environ[\"AZDATA_PASSWORD\"] = mssql_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 {mssql_cluster_name} ')\n",
|
||||
"run_command(f'azdata bdc create -c {mssql_target_profile}')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "c43ea026-ca5e-4e2a-8602-fcc786354168",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Login to SQL Server 2019 Big Data Cluster**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "9c5428f4-08b9-4799-a35d-867c91dc29fb"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"run_command(f'azdata login --namespace {mssql_cluster_name}')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "5120c387-1088-435b-856e-e59f147c45a2",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Show SQL Server 2019 Big Data Cluster endpoints**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "97974eda-e108-4c21-a58e-c6bb58f14ef1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"from IPython.display import *\n",
|
||||
"pandas.set_option('display.max_colwidth', -1)\n",
|
||||
"cmd = f'azdata bdc endpoint list'\n",
|
||||
"cmdOutput = !{cmd}\n",
|
||||
"endpoints = json.loads(''.join(cmdOutput))\n",
|
||||
"endpointsDataFrame = pandas.DataFrame(endpoints)\n",
|
||||
"endpointsDataFrame.columns = [' '.join(word[0].upper() + word[1:] for word in columnName.split()) for columnName in endpoints[0].keys()]\n",
|
||||
"display(HTML(endpointsDataFrame.to_html(index=False, render_links=True)))"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "9a5d0aef-a8da-4845-b470-d714435f0304",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Connect to SQL Server Master instance in Azure Data Studio**\n",
|
||||
"Click the link below to connect to the SQL Server Master instance of the SQL Server 2019 Big Data Cluster."
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "4a49b629-bd7a-43ba-bf18-6cdc0737b0f9"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"sqlEndpoints = [x for x in endpoints if x['name'] == 'sql-server-master']\n",
|
||||
"if sqlEndpoints and len(sqlEndpoints) == 1:\n",
|
||||
" connectionParameter = '{\"serverName\":\"' + sqlEndpoints[0]['endpoint'] + '\",\"providerName\":\"MSSQL\",\"authenticationType\":\"SqlLogin\",\"userName\":' + json.dumps(mssql_username) + ',\"password\":' + json.dumps(mssql_password) + '}'\n",
|
||||
" display(HTML('<br/><a href=\"command:azdata.connect?' + html.escape(connectionParameter)+'\"><font size=\"3\">Click here to connect to SQL Server Master instance</font></a><br/>'))\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>'))\n",
|
||||
"else:\n",
|
||||
" sys.exit('Could not find the SQL Server Master instance endpoint.')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "1c9d1f2c-62ba-4070-920a-d30b67bdcc7c",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
{
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"name": "python3",
|
||||
"display_name": "Python 3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.6.6",
|
||||
"mimetype": "text/x-python",
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"pygments_lexer": "ipython3",
|
||||
"nbconvert_exporter": "python",
|
||||
"file_extension": ".py"
|
||||
}
|
||||
},
|
||||
"nbformat_minor": 2,
|
||||
"nbformat": 4,
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"\n",
|
||||
" \n",
|
||||
"## Deploy SQL Server 2019 Big Data Cluster on an existing Azure Kubernetes Service (AKS) cluster\n",
|
||||
" \n",
|
||||
"This notebook walks through the process of deploying a <a href=\"https://docs.microsoft.com/sql/big-data-cluster/big-data-cluster-overview?view=sqlallproducts-allversions\">SQL Server 2019 Big Data Cluster</a> on an existing AKS cluster.\n",
|
||||
" \n",
|
||||
"* Follow the instructions in the **Prerequisites** cell to install the tools if not already installed.\n",
|
||||
"* The **Required information** will check and prompt you for password if it is not set in the environment variable. The password can be used to access the cluster controller, SQL Server, and Knox.\n",
|
||||
"\n",
|
||||
"<span style=\"color:red\"><font size=\"3\">Please press the \"Run all\" button to run the notebook</font></span>"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "82e60c1a-7acf-47ee-877f-9e85e92e11da"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Prerequisites** \n",
|
||||
"Ensure the following tools are installed and added to PATH before proceeding.\n",
|
||||
" \n",
|
||||
"|Tools|Description|Installation|\n",
|
||||
"|---|---|---|\n",
|
||||
"|kubectl | Command-line tool for monitoring the underlying Kubernetes cluster | [Installation](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl-binary-using-native-package-management) |\n",
|
||||
"|azdata | Command-line tool for installing and managing a Big Data Cluster |[Installation](https://docs.microsoft.com/en-us/sql/big-data-cluster/deploy-install-azdata?view=sqlallproducts-allversions) |"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "714582b9-10ee-409e-ab12-15a4825c9471"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Setup**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "e3dd8e75-e15f-44b4-81fc-1f54d6f0b1e2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"import pandas,sys,os,json,html,getpass,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",
|
||||
"def run_command(command):\n",
|
||||
" print(\"Executing: \" + command)\n",
|
||||
" !{command}\n",
|
||||
" if _exit_code != 0:\n",
|
||||
" sys.exit(f'Command execution failed with exit code: {str(_exit_code)}.\\n\\t{command}\\n')\n",
|
||||
" print(f'Successfully executed: {command}')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "d973d5b4-7f0a-4a9d-b204-a16480f3940d",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Set variables**\n",
|
||||
"Generated by Azure Data Studio using the values collected in the Deploy Big Data Cluster wizard"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "4b266b2d-bd1b-4565-92c9-3fc146cdce6d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Check dependencies**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "2544648b-59c9-4ce5-a3b6-87086e214d4c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"run_command('kubectl version --client=true')\n",
|
||||
"run_command('azdata --version')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "691671d7-3f05-406c-a183-4cff7d17f83d",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Required information**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "0bb02e76-fee8-4dbc-a75b-d5b9d1b187d0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"invoked_by_wizard = \"AZDATA_NB_VAR_BDC_ADMIN_PASSWORD\" in os.environ\n",
|
||||
"if invoked_by_wizard:\n",
|
||||
" mssql_password = os.environ[\"AZDATA_NB_VAR_BDC_ADMIN_PASSWORD\"]\n",
|
||||
"else:\n",
|
||||
" mssql_password = getpass.getpass(prompt = 'SQL Server 2019 Big Data Cluster controller password')\n",
|
||||
" if mssql_password == \"\":\n",
|
||||
" sys.exit(f'Password is required.')\n",
|
||||
" confirm_password = getpass.getpass(prompt = 'Confirm password')\n",
|
||||
" if mssql_password != confirm_password:\n",
|
||||
" sys.exit(f'Passwords do not match.')\n",
|
||||
"print('You can also use the controller password to access Knox and SQL Server.')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "e7e10828-6cae-45af-8c2f-1484b6d4f9ac",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Set and show current context**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "127c8042-181f-4862-a390-96e59c181d09"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"run_command(f'kubectl config use-context {mssql_cluster_context}')\n",
|
||||
"run_command('kubectl config current-context')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "7d1a03d4-1df8-48eb-bff0-0042603b95b1",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Create deployment configuration files**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "138536c3-1db6-428f-9e5c-8269a02fb52e"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"mssql_target_profile = 'ads-bdc-custom-profile'\n",
|
||||
"if not os.path.exists(mssql_target_profile):\n",
|
||||
" os.mkdir(mssql_target_profile)\n",
|
||||
"bdcJsonObj = json.loads(bdc_json)\n",
|
||||
"controlJsonObj = json.loads(control_json)\n",
|
||||
"bdcJsonFile = open(f'{mssql_target_profile}/bdc.json', 'w')\n",
|
||||
"bdcJsonFile.write(json.dumps(bdcJsonObj, indent = 4))\n",
|
||||
"bdcJsonFile.close()\n",
|
||||
"controlJsonFile = open(f'{mssql_target_profile}/control.json', 'w')\n",
|
||||
"controlJsonFile.write(json.dumps(controlJsonObj, indent = 4))\n",
|
||||
"controlJsonFile.close()\n",
|
||||
"print(f'Created deployment configuration folder: {mssql_target_profile}')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "2ff82c8a-4bce-449c-9d91-3ac7dd272021",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Create SQL Server 2019 Big Data Cluster**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "efe78cd3-ed73-4c9b-b586-fdd6c07dd37f"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"print (f'Creating SQL Server 2019 Big Data Cluster: {mssql_cluster_name} using configuration {mssql_target_profile}')\n",
|
||||
"os.environ[\"ACCEPT_EULA\"] = 'yes'\n",
|
||||
"os.environ[\"AZDATA_USERNAME\"] = mssql_username\n",
|
||||
"os.environ[\"AZDATA_PASSWORD\"] = mssql_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 {mssql_cluster_name} ')\n",
|
||||
"run_command(f'azdata bdc create -c {mssql_target_profile}')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "373947a1-90b9-49ee-86f4-17a4c7d4ca76",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Login to SQL Server 2019 Big Data Cluster**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "4e026d39-12d4-4c80-8e30-de2b782f2110"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"run_command(f'azdata login --namespace {mssql_cluster_name}')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "79adda27-371d-4dcb-b867-db025f8162a5",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Show SQL Server 2019 Big Data Cluster endpoints**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "c1921288-ad11-40d8-9aea-127a722b3df8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"from IPython.display import *\n",
|
||||
"pandas.set_option('display.max_colwidth', -1)\n",
|
||||
"cmd = f'azdata bdc endpoint list'\n",
|
||||
"cmdOutput = !{cmd}\n",
|
||||
"endpoints = json.loads(''.join(cmdOutput))\n",
|
||||
"endpointsDataFrame = pandas.DataFrame(endpoints)\n",
|
||||
"endpointsDataFrame.columns = [' '.join(word[0].upper() + word[1:] for word in columnName.split()) for columnName in endpoints[0].keys()]\n",
|
||||
"display(HTML(endpointsDataFrame.to_html(index=False, render_links=True)))"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "a2202494-fd6c-4534-987d-15c403a5096f",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Connect to SQL Server Master instance in Azure Data Studio**\n",
|
||||
"Click the link below to connect to the SQL Server Master instance of the SQL Server 2019 Big Data Cluster."
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "621863a2-aa61-46f4-a9d0-717f41c009ee"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"sqlEndpoints = [x for x in endpoints if x['name'] == 'sql-server-master']\n",
|
||||
"if sqlEndpoints and len(sqlEndpoints) == 1:\n",
|
||||
" connectionParameter = '{\"serverName\":\"' + sqlEndpoints[0]['endpoint'] + '\",\"providerName\":\"MSSQL\",\"authenticationType\":\"SqlLogin\",\"userName\":' + json.dumps(mssql_username) + ',\"password\":' + json.dumps(mssql_password) + '}'\n",
|
||||
" display(HTML('<br/><a href=\"command:azdata.connect?' + html.escape(connectionParameter)+'\"><font size=\"3\">Click here to connect to SQL Server Master instance</font></a><br/>'))\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>'))\n",
|
||||
"else:\n",
|
||||
" sys.exit('Could not find the SQL Server Master instance endpoint.')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "48342355-9d2b-4fa6-b1aa-3bc77d434dfa",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
{
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"name": "python3",
|
||||
"display_name": "Python 3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.6.6",
|
||||
"mimetype": "text/x-python",
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"pygments_lexer": "ipython3",
|
||||
"nbconvert_exporter": "python",
|
||||
"file_extension": ".py"
|
||||
}
|
||||
},
|
||||
"nbformat_minor": 2,
|
||||
"nbformat": 4,
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"\n",
|
||||
" \n",
|
||||
"## Deploy SQL Server 2019 Big Data Cluster on an existing Azure Red Hat OpenShift cluster\n",
|
||||
" \n",
|
||||
"This notebook walks through the process of deploying a <a href=\"https://docs.microsoft.com/sql/big-data-cluster/big-data-cluster-overview?view=sqlallproducts-allversions\">SQL Server 2019 Big Data Cluster</a> on an existing Azure Red Hat OpenShift cluster.\n",
|
||||
" \n",
|
||||
"* Follow the instructions in the **Prerequisites** cell to install the tools if not already installed.\n",
|
||||
"* The **Required information** will check and prompt you for password if it is not set in the environment variable. The password can be used to access the cluster controller, SQL Server, and Knox.\n",
|
||||
"\n",
|
||||
"<span style=\"color:red\"><font size=\"3\">Please press the \"Run all\" button to run the notebook</font></span>"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "23954d96-3932-4a8e-ab73-da605f99b1a4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Prerequisites** \n",
|
||||
"Ensure the following tools are installed and added to PATH before proceeding.\n",
|
||||
" \n",
|
||||
"|Tools|Description|Installation|\n",
|
||||
"|---|---|---|\n",
|
||||
"|kubectl | Command-line tool for monitoring the underlying Kubernetes cluster | [Installation](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl-binary-using-native-package-management) |\n",
|
||||
"|azdata | Command-line tool for installing and managing a Big Data Cluster |[Installation](https://docs.microsoft.com/en-us/sql/big-data-cluster/deploy-install-azdata?view=sqlallproducts-allversions) |"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "1d7f4c6a-0cb8-4ecc-81c8-544712253a3f"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Setup**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "a31f9894-903f-4e19-a5a8-6fd888ff013b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"import pandas,sys,os,json,html,getpass,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",
|
||||
"def run_command(command):\n",
|
||||
" print(\"Executing: \" + command)\n",
|
||||
" !{command}\n",
|
||||
" if _exit_code != 0:\n",
|
||||
" sys.exit(f'Command execution failed with exit code: {str(_exit_code)}.\\n\\t{command}\\n')\n",
|
||||
" print(f'Successfully executed: {command}')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "26fa8bc4-4b8e-4c31-ae11-50484821cea8",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Set variables**\n",
|
||||
"Generated by Azure Data Studio using the values collected in the Deploy Big Data Cluster wizard"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "e70640d0-6059-4cab-939e-e985a978c0da"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Check dependencies**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "869d0397-a280-4dc4-be76-d652189b5131"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"run_command('kubectl version --client=true')\n",
|
||||
"run_command('azdata --version')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "c38afb67-1132-495e-9af1-35bf067acbeb",
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Required information**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "7b383b0d-5687-45b3-a16f-ba3b170c796e"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"invoked_by_wizard = \"AZDATA_NB_VAR_BDC_ADMIN_PASSWORD\" in os.environ\n",
|
||||
"if invoked_by_wizard:\n",
|
||||
" mssql_password = os.environ[\"AZDATA_NB_VAR_BDC_ADMIN_PASSWORD\"]\n",
|
||||
" if mssql_auth_mode == \"ad\":\n",
|
||||
" mssql_domain_service_account_password = os.environ[\"AZDATA_NB_VAR_BDC_AD_DOMAIN_SVC_PASSWORD\"]\n",
|
||||
"else:\n",
|
||||
" mssql_password = getpass.getpass(prompt = 'SQL Server 2019 Big Data Cluster controller password')\n",
|
||||
" if mssql_password == \"\":\n",
|
||||
" sys.exit(f'Password is required.')\n",
|
||||
" confirm_password = getpass.getpass(prompt = 'Confirm password')\n",
|
||||
" if mssql_password != confirm_password:\n",
|
||||
" sys.exit(f'Passwords do not match.')\n",
|
||||
" if mssql_auth_mode == \"ad\":\n",
|
||||
" mssql_domain_service_account_password = getpass.getpass(prompt = 'Domain service account password')\n",
|
||||
" if mssql_domain_service_account_password == \"\":\n",
|
||||
" sys.exit(f'Domain service account password is required.')\n",
|
||||
"print('You can also use the controller password to access Knox and SQL Server.')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "b5970f2b-cf13-41af-b0a2-5133d840325e",
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Set and show current context**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "6456bd0c-5b64-4d76-be59-e3a5b32697f5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"run_command(f'kubectl config use-context {mssql_cluster_context}')\n",
|
||||
"run_command('kubectl config current-context')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "a38f8b3a-f93a-484c-b9e2-4eba3ed99cc2"
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Make sure the target namespace already exists**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "3bf1d902-2217-4c99-b2d6-38e45de8e308"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"run_command(f'kubectl get namespace {mssql_cluster_name}')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "6ca9bf71-049a-458e-8000-311d4c15b1ca"
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Create deployment configuration files**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "6d78da36-6af5-4309-baad-bc81bb2cdb7f"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"mssql_target_profile = 'ads-bdc-custom-profile'\n",
|
||||
"if not os.path.exists(mssql_target_profile):\n",
|
||||
" os.mkdir(mssql_target_profile)\n",
|
||||
"bdcJsonObj = json.loads(bdc_json)\n",
|
||||
"controlJsonObj = json.loads(control_json)\n",
|
||||
"bdcJsonFile = open(f'{mssql_target_profile}/bdc.json', 'w')\n",
|
||||
"bdcJsonFile.write(json.dumps(bdcJsonObj, indent = 4))\n",
|
||||
"bdcJsonFile.close()\n",
|
||||
"controlJsonFile = open(f'{mssql_target_profile}/control.json', 'w')\n",
|
||||
"controlJsonFile.write(json.dumps(controlJsonObj, indent = 4))\n",
|
||||
"controlJsonFile.close()\n",
|
||||
"print(f'Created deployment configuration folder: {mssql_target_profile}')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "3110ab23-ecfc-4e36-a1c5-28536b7edebf",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Create SQL Server 2019 Big Data Cluster**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "7d56d262-8cd5-49e4-b745-332c6e7a3cb2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"print (f'Creating SQL Server 2019 Big Data Cluster: {mssql_cluster_name} using configuration {mssql_target_profile}')\n",
|
||||
"os.environ[\"ACCEPT_EULA\"] = 'yes'\n",
|
||||
"os.environ[\"AZDATA_USERNAME\"] = mssql_username\n",
|
||||
"os.environ[\"AZDATA_PASSWORD\"] = mssql_password\n",
|
||||
"if mssql_auth_mode == \"ad\":\n",
|
||||
" os.environ[\"DOMAIN_SERVICE_ACCOUNT_USERNAME\"] = mssql_domain_service_account_username\n",
|
||||
" os.environ[\"DOMAIN_SERVICE_ACCOUNT_PASSWORD\"] = mssql_domain_service_account_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 {mssql_cluster_name} ')\n",
|
||||
"run_command(f'azdata bdc create -c {mssql_target_profile}')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "0a743e88-e7d0-4b41-b8a3-e43985d15f2b",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Login to SQL Server 2019 Big Data Cluster**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "7929fd90-324d-482a-a101-ae29cb183691"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"run_command(f'azdata login --namespace {mssql_cluster_name}')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "3a49909b-e09e-4e62-a825-c39de2cffc94",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Show SQL Server 2019 Big Data Cluster endpoints**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "038e801a-a393-4f8d-8e2d-97bc3b740b0c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"from IPython.display import *\n",
|
||||
"pandas.set_option('display.max_colwidth', -1)\n",
|
||||
"cmd = f'azdata bdc endpoint list'\n",
|
||||
"cmdOutput = !{cmd}\n",
|
||||
"endpoints = json.loads(''.join(cmdOutput))\n",
|
||||
"endpointsDataFrame = pandas.DataFrame(endpoints)\n",
|
||||
"endpointsDataFrame.columns = [' '.join(word[0].upper() + word[1:] for word in columnName.split()) for columnName in endpoints[0].keys()]\n",
|
||||
"display(HTML(endpointsDataFrame.to_html(index=False, render_links=True)))"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "2a8c8d5d-862c-4672-9309-38aa03afc4e6",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Connect to SQL Server Master instance in Azure Data Studio**\n",
|
||||
"Click the link below to connect to the SQL Server Master instance of the SQL Server 2019 Big Data Cluster."
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "0bd809fa-8225-4954-a50c-da57ea167896"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"sqlEndpoints = [x for x in endpoints if x['name'] == 'sql-server-master']\n",
|
||||
"if sqlEndpoints and len(sqlEndpoints) == 1:\n",
|
||||
" connectionParameter = '{\"serverName\":\"' + sqlEndpoints[0]['endpoint'] + '\",\"providerName\":\"MSSQL\",\"authenticationType\":\"SqlLogin\",\"userName\":' + json.dumps(mssql_username) + ',\"password\":' + json.dumps(mssql_password) + '}'\n",
|
||||
" display(HTML('<br/><a href=\"command:azdata.connect?' + html.escape(connectionParameter)+'\"><font size=\"3\">Click here to connect to SQL Server Master instance</font></a><br/>'))\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>'))\n",
|
||||
"else:\n",
|
||||
" sys.exit('Could not find the SQL Server Master instance endpoint.')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "d591785d-71aa-4c5d-9cbb-a7da79bca503",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
{
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"name": "python3",
|
||||
"display_name": "Python 3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.6.6",
|
||||
"mimetype": "text/x-python",
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"pygments_lexer": "ipython3",
|
||||
"nbconvert_exporter": "python",
|
||||
"file_extension": ".py"
|
||||
}
|
||||
},
|
||||
"nbformat_minor": 2,
|
||||
"nbformat": 4,
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"\n",
|
||||
" \n",
|
||||
"## Deploy SQL Server 2019 Big Data Cluster on an existing cluster deployed using kubeadm\n",
|
||||
" \n",
|
||||
"This notebook walks through the process of deploying a <a href=\"https://docs.microsoft.com/sql/big-data-cluster/big-data-cluster-overview?view=sqlallproducts-allversions\">SQL Server 2019 Big Data Cluster</a> on an existing kubeadm cluster.\n",
|
||||
" \n",
|
||||
"* Follow the instructions in the **Prerequisites** cell to install the tools if not already installed.\n",
|
||||
"* The **Required information** will check and prompt you for password if it is not set in the environment variable. The password can be used to access the cluster controller, SQL Server, and Knox.\n",
|
||||
"\n",
|
||||
"<span style=\"color:red\"><font size=\"3\">Please press the \"Run all\" button to run the notebook</font></span>"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "23954d96-3932-4a8e-ab73-da605f99b1a4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Prerequisites** \n",
|
||||
"Ensure the following tools are installed and added to PATH before proceeding.\n",
|
||||
" \n",
|
||||
"|Tools|Description|Installation|\n",
|
||||
"|---|---|---|\n",
|
||||
"|kubectl | Command-line tool for monitoring the underlying Kubernetes cluster | [Installation](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl-binary-using-native-package-management) |\n",
|
||||
"|azdata | Command-line tool for installing and managing a Big Data Cluster |[Installation](https://docs.microsoft.com/en-us/sql/big-data-cluster/deploy-install-azdata?view=sqlallproducts-allversions) |"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "1d7f4c6a-0cb8-4ecc-81c8-544712253a3f"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Setup**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "a31f9894-903f-4e19-a5a8-6fd888ff013b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"import pandas,sys,os,json,html,getpass,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",
|
||||
"def run_command(command):\n",
|
||||
" print(\"Executing: \" + command)\n",
|
||||
" !{command}\n",
|
||||
" if _exit_code != 0:\n",
|
||||
" sys.exit(f'Command execution failed with exit code: {str(_exit_code)}.\\n\\t{command}\\n')\n",
|
||||
" print(f'Successfully executed: {command}')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "26fa8bc4-4b8e-4c31-ae11-50484821cea8",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Set variables**\n",
|
||||
"Generated by Azure Data Studio using the values collected in the Deploy Big Data Cluster wizard"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "e70640d0-6059-4cab-939e-e985a978c0da"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Check dependencies**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "869d0397-a280-4dc4-be76-d652189b5131"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"run_command('kubectl version --client=true')\n",
|
||||
"run_command('azdata --version')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "c38afb67-1132-495e-9af1-35bf067acbeb",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Required information**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "7b383b0d-5687-45b3-a16f-ba3b170c796e"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"invoked_by_wizard = \"AZDATA_NB_VAR_BDC_ADMIN_PASSWORD\" in os.environ\n",
|
||||
"if invoked_by_wizard:\n",
|
||||
" mssql_password = os.environ[\"AZDATA_NB_VAR_BDC_ADMIN_PASSWORD\"]\n",
|
||||
" if mssql_auth_mode == \"ad\":\n",
|
||||
" mssql_domain_service_account_password = os.environ[\"AZDATA_NB_VAR_BDC_AD_DOMAIN_SVC_PASSWORD\"]\n",
|
||||
"else:\n",
|
||||
" mssql_password = getpass.getpass(prompt = 'SQL Server 2019 Big Data Cluster controller password')\n",
|
||||
" if mssql_password == \"\":\n",
|
||||
" sys.exit(f'Password is required.')\n",
|
||||
" confirm_password = getpass.getpass(prompt = 'Confirm password')\n",
|
||||
" if mssql_password != confirm_password:\n",
|
||||
" sys.exit(f'Passwords do not match.')\n",
|
||||
" if mssql_auth_mode == \"ad\":\n",
|
||||
" mssql_domain_service_account_password = getpass.getpass(prompt = 'Domain service account password')\n",
|
||||
" if mssql_domain_service_account_password == \"\":\n",
|
||||
" sys.exit(f'Domain service account password is required.')\n",
|
||||
"print('You can also use the controller password to access Knox and SQL Server.')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "b5970f2b-cf13-41af-b0a2-5133d840325e",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Set and show current context**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "6456bd0c-5b64-4d76-be59-e3a5b32697f5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"run_command(f'kubectl config use-context {mssql_cluster_context}')\n",
|
||||
"run_command('kubectl config current-context')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "a38f8b3a-f93a-484c-b9e2-4eba3ed99cc2"
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Create deployment configuration files**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "6d78da36-6af5-4309-baad-bc81bb2cdb7f"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"mssql_target_profile = 'ads-bdc-custom-profile'\n",
|
||||
"if not os.path.exists(mssql_target_profile):\n",
|
||||
" os.mkdir(mssql_target_profile)\n",
|
||||
"bdcJsonObj = json.loads(bdc_json)\n",
|
||||
"controlJsonObj = json.loads(control_json)\n",
|
||||
"bdcJsonFile = open(f'{mssql_target_profile}/bdc.json', 'w')\n",
|
||||
"bdcJsonFile.write(json.dumps(bdcJsonObj, indent = 4))\n",
|
||||
"bdcJsonFile.close()\n",
|
||||
"controlJsonFile = open(f'{mssql_target_profile}/control.json', 'w')\n",
|
||||
"controlJsonFile.write(json.dumps(controlJsonObj, indent = 4))\n",
|
||||
"controlJsonFile.close()\n",
|
||||
"print(f'Created deployment configuration folder: {mssql_target_profile}')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "3110ab23-ecfc-4e36-a1c5-28536b7edebf",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Create SQL Server 2019 Big Data Cluster**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "7d56d262-8cd5-49e4-b745-332c6e7a3cb2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"print (f'Creating SQL Server 2019 Big Data Cluster: {mssql_cluster_name} using configuration {mssql_target_profile}')\n",
|
||||
"os.environ[\"ACCEPT_EULA\"] = 'yes'\n",
|
||||
"os.environ[\"AZDATA_USERNAME\"] = mssql_username\n",
|
||||
"os.environ[\"AZDATA_PASSWORD\"] = mssql_password\n",
|
||||
"if mssql_auth_mode == \"ad\":\n",
|
||||
" os.environ[\"DOMAIN_SERVICE_ACCOUNT_USERNAME\"] = mssql_domain_service_account_username\n",
|
||||
" os.environ[\"DOMAIN_SERVICE_ACCOUNT_PASSWORD\"] = mssql_domain_service_account_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 {mssql_cluster_name} ')\n",
|
||||
"run_command(f'azdata bdc create -c {mssql_target_profile}')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "0a743e88-e7d0-4b41-b8a3-e43985d15f2b",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Login to SQL Server 2019 Big Data Cluster**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "7929fd90-324d-482a-a101-ae29cb183691"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"run_command(f'azdata login --namespace {mssql_cluster_name}')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "3a49909b-e09e-4e62-a825-c39de2cffc94",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Show SQL Server 2019 Big Data Cluster endpoints**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "038e801a-a393-4f8d-8e2d-97bc3b740b0c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"from IPython.display import *\n",
|
||||
"pandas.set_option('display.max_colwidth', -1)\n",
|
||||
"cmd = f'azdata bdc endpoint list'\n",
|
||||
"cmdOutput = !{cmd}\n",
|
||||
"endpoints = json.loads(''.join(cmdOutput))\n",
|
||||
"endpointsDataFrame = pandas.DataFrame(endpoints)\n",
|
||||
"endpointsDataFrame.columns = [' '.join(word[0].upper() + word[1:] for word in columnName.split()) for columnName in endpoints[0].keys()]\n",
|
||||
"display(HTML(endpointsDataFrame.to_html(index=False, render_links=True)))"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "2a8c8d5d-862c-4672-9309-38aa03afc4e6",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Connect to SQL Server Master instance in Azure Data Studio**\n",
|
||||
"Click the link below to connect to the SQL Server Master instance of the SQL Server 2019 Big Data Cluster."
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "0bd809fa-8225-4954-a50c-da57ea167896"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"sqlEndpoints = [x for x in endpoints if x['name'] == 'sql-server-master']\n",
|
||||
"if sqlEndpoints and len(sqlEndpoints) == 1:\n",
|
||||
" connectionParameter = '{\"serverName\":\"' + sqlEndpoints[0]['endpoint'] + '\",\"providerName\":\"MSSQL\",\"authenticationType\":\"SqlLogin\",\"userName\":' + json.dumps(mssql_username) + ',\"password\":' + json.dumps(mssql_password) + '}'\n",
|
||||
" display(HTML('<br/><a href=\"command:azdata.connect?' + html.escape(connectionParameter)+'\"><font size=\"3\">Click here to connect to SQL Server Master instance</font></a><br/>'))\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>'))\n",
|
||||
"else:\n",
|
||||
" sys.exit('Could not find the SQL Server Master instance endpoint.')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "d591785d-71aa-4c5d-9cbb-a7da79bca503",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
{
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"name": "python3",
|
||||
"display_name": "Python 3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.6.6",
|
||||
"mimetype": "text/x-python",
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"pygments_lexer": "ipython3",
|
||||
"nbconvert_exporter": "python",
|
||||
"file_extension": ".py"
|
||||
}
|
||||
},
|
||||
"nbformat_minor": 2,
|
||||
"nbformat": 4,
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"\n",
|
||||
" \n",
|
||||
"## Deploy SQL Server 2019 Big Data Cluster on an existing OpenShift cluster\n",
|
||||
" \n",
|
||||
"This notebook walks through the process of deploying a <a href=\"https://docs.microsoft.com/sql/big-data-cluster/big-data-cluster-overview?view=sqlallproducts-allversions\">SQL Server 2019 Big Data Cluster</a> on an existing OpenShift cluster.\n",
|
||||
" \n",
|
||||
"* Follow the instructions in the **Prerequisites** cell to install the tools if not already installed.\n",
|
||||
"* The **Required information** will check and prompt you for password if it is not set in the environment variable. The password can be used to access the cluster controller, SQL Server, and Knox.\n",
|
||||
"\n",
|
||||
"<span style=\"color:red\"><font size=\"3\">Please press the \"Run all\" button to run the notebook</font></span>"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "23954d96-3932-4a8e-ab73-da605f99b1a4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Prerequisites** \n",
|
||||
"Ensure the following tools are installed and added to PATH before proceeding.\n",
|
||||
" \n",
|
||||
"|Tools|Description|Installation|\n",
|
||||
"|---|---|---|\n",
|
||||
"|kubectl | Command-line tool for monitoring the underlying Kubernetes cluster | [Installation](https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl-binary-using-native-package-management) |\n",
|
||||
"|azdata | Command-line tool for installing and managing a Big Data Cluster |[Installation](https://docs.microsoft.com/en-us/sql/big-data-cluster/deploy-install-azdata?view=sqlallproducts-allversions) |"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "1d7f4c6a-0cb8-4ecc-81c8-544712253a3f"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Setup**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "a31f9894-903f-4e19-a5a8-6fd888ff013b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"import pandas,sys,os,json,html,getpass,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",
|
||||
"def run_command(command):\n",
|
||||
" print(\"Executing: \" + command)\n",
|
||||
" !{command}\n",
|
||||
" if _exit_code != 0:\n",
|
||||
" sys.exit(f'Command execution failed with exit code: {str(_exit_code)}.\\n\\t{command}\\n')\n",
|
||||
" print(f'Successfully executed: {command}')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "26fa8bc4-4b8e-4c31-ae11-50484821cea8",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Set variables**\n",
|
||||
"Generated by Azure Data Studio using the values collected in the Deploy Big Data Cluster wizard"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "e70640d0-6059-4cab-939e-e985a978c0da"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Check dependencies**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "869d0397-a280-4dc4-be76-d652189b5131"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"run_command('kubectl version --client=true')\n",
|
||||
"run_command('azdata --version')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "c38afb67-1132-495e-9af1-35bf067acbeb",
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Required information**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "7b383b0d-5687-45b3-a16f-ba3b170c796e"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"invoked_by_wizard = \"AZDATA_NB_VAR_BDC_ADMIN_PASSWORD\" in os.environ\n",
|
||||
"if invoked_by_wizard:\n",
|
||||
" mssql_password = os.environ[\"AZDATA_NB_VAR_BDC_ADMIN_PASSWORD\"]\n",
|
||||
" if mssql_auth_mode == \"ad\":\n",
|
||||
" mssql_domain_service_account_password = os.environ[\"AZDATA_NB_VAR_BDC_AD_DOMAIN_SVC_PASSWORD\"]\n",
|
||||
"else:\n",
|
||||
" mssql_password = getpass.getpass(prompt = 'SQL Server 2019 Big Data Cluster controller password')\n",
|
||||
" if mssql_password == \"\":\n",
|
||||
" sys.exit(f'Password is required.')\n",
|
||||
" confirm_password = getpass.getpass(prompt = 'Confirm password')\n",
|
||||
" if mssql_password != confirm_password:\n",
|
||||
" sys.exit(f'Passwords do not match.')\n",
|
||||
" if mssql_auth_mode == \"ad\":\n",
|
||||
" mssql_domain_service_account_password = getpass.getpass(prompt = 'Domain service account password')\n",
|
||||
" if mssql_domain_service_account_password == \"\":\n",
|
||||
" sys.exit(f'Domain service account password is required.')\n",
|
||||
"print('You can also use the controller password to access Knox and SQL Server.')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "b5970f2b-cf13-41af-b0a2-5133d840325e",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Set and show current context**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "6456bd0c-5b64-4d76-be59-e3a5b32697f5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"run_command(f'kubectl config use-context {mssql_cluster_context}')\n",
|
||||
"run_command('kubectl config current-context')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "a38f8b3a-f93a-484c-b9e2-4eba3ed99cc2"
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Make sure the target namespace already exists**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "b903f09b-0eeb-45c0-8173-1741cce3790c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"run_command(f'kubectl get namespace {mssql_cluster_name}')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "174c02ea-8876-43be-bd93-3a39223e25ec"
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Create deployment configuration files**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "6d78da36-6af5-4309-baad-bc81bb2cdb7f"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"mssql_target_profile = 'ads-bdc-custom-profile'\n",
|
||||
"if not os.path.exists(mssql_target_profile):\n",
|
||||
" os.mkdir(mssql_target_profile)\n",
|
||||
"bdcJsonObj = json.loads(bdc_json)\n",
|
||||
"controlJsonObj = json.loads(control_json)\n",
|
||||
"bdcJsonFile = open(f'{mssql_target_profile}/bdc.json', 'w')\n",
|
||||
"bdcJsonFile.write(json.dumps(bdcJsonObj, indent = 4))\n",
|
||||
"bdcJsonFile.close()\n",
|
||||
"controlJsonFile = open(f'{mssql_target_profile}/control.json', 'w')\n",
|
||||
"controlJsonFile.write(json.dumps(controlJsonObj, indent = 4))\n",
|
||||
"controlJsonFile.close()\n",
|
||||
"print(f'Created deployment configuration folder: {mssql_target_profile}')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "3110ab23-ecfc-4e36-a1c5-28536b7edebf",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Create SQL Server 2019 Big Data Cluster**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "7d56d262-8cd5-49e4-b745-332c6e7a3cb2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"print (f'Creating SQL Server 2019 Big Data Cluster: {mssql_cluster_name} using configuration {mssql_target_profile}')\n",
|
||||
"os.environ[\"ACCEPT_EULA\"] = 'yes'\n",
|
||||
"os.environ[\"AZDATA_USERNAME\"] = mssql_username\n",
|
||||
"os.environ[\"AZDATA_PASSWORD\"] = mssql_password\n",
|
||||
"if mssql_auth_mode == \"ad\":\n",
|
||||
" os.environ[\"DOMAIN_SERVICE_ACCOUNT_USERNAME\"] = mssql_domain_service_account_username\n",
|
||||
" os.environ[\"DOMAIN_SERVICE_ACCOUNT_PASSWORD\"] = mssql_domain_service_account_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 {mssql_cluster_name} ')\n",
|
||||
"run_command(f'azdata bdc create -c {mssql_target_profile}')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "0a743e88-e7d0-4b41-b8a3-e43985d15f2b",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Login to SQL Server 2019 Big Data Cluster**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "7929fd90-324d-482a-a101-ae29cb183691"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"run_command(f'azdata login --namespace {mssql_cluster_name}')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "3a49909b-e09e-4e62-a825-c39de2cffc94",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Show SQL Server 2019 Big Data Cluster endpoints**"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "038e801a-a393-4f8d-8e2d-97bc3b740b0c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"from IPython.display import *\n",
|
||||
"pandas.set_option('display.max_colwidth', -1)\n",
|
||||
"cmd = f'azdata bdc endpoint list'\n",
|
||||
"cmdOutput = !{cmd}\n",
|
||||
"endpoints = json.loads(''.join(cmdOutput))\n",
|
||||
"endpointsDataFrame = pandas.DataFrame(endpoints)\n",
|
||||
"endpointsDataFrame.columns = [' '.join(word[0].upper() + word[1:] for word in columnName.split()) for columnName in endpoints[0].keys()]\n",
|
||||
"display(HTML(endpointsDataFrame.to_html(index=False, render_links=True)))"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "2a8c8d5d-862c-4672-9309-38aa03afc4e6",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"### **Connect to SQL Server Master instance in Azure Data Studio**\n",
|
||||
"Click the link below to connect to the SQL Server Master instance of the SQL Server 2019 Big Data Cluster."
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "0bd809fa-8225-4954-a50c-da57ea167896"
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"sqlEndpoints = [x for x in endpoints if x['name'] == 'sql-server-master']\n",
|
||||
"if sqlEndpoints and len(sqlEndpoints) == 1:\n",
|
||||
" connectionParameter = '{\"serverName\":\"' + sqlEndpoints[0]['endpoint'] + '\",\"providerName\":\"MSSQL\",\"authenticationType\":\"SqlLogin\",\"userName\":' + json.dumps(mssql_username) + ',\"password\":' + json.dumps(mssql_password) + '}'\n",
|
||||
" display(HTML('<br/><a href=\"command:azdata.connect?' + html.escape(connectionParameter)+'\"><font size=\"3\">Click here to connect to SQL Server Master instance</font></a><br/>'))\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>'))\n",
|
||||
"else:\n",
|
||||
" sys.exit('Could not find the SQL Server Master instance endpoint.')"
|
||||
],
|
||||
"metadata": {
|
||||
"azdata_cell_guid": "d591785d-71aa-4c5d-9cbb-a7da79bca503",
|
||||
"tags": [
|
||||
"hide_input"
|
||||
]
|
||||
},
|
||||
"outputs": [],
|
||||
"execution_count": null
|
||||
}
|
||||
]
|
||||
}
|
||||
369
extensions/big-data-cluster/package.json
Normal file
@@ -0,0 +1,369 @@
|
||||
{
|
||||
"name": "big-data-cluster",
|
||||
"displayName": "%text.sqlServerBigDataClusters%",
|
||||
"description": "%description%",
|
||||
"version": "1.0.0",
|
||||
"publisher": "Microsoft",
|
||||
"preview": true,
|
||||
"license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt",
|
||||
"icon": "images/extension.png",
|
||||
"engines": {
|
||||
"vscode": "*",
|
||||
"azdata": "*"
|
||||
},
|
||||
"activationEvents": [
|
||||
"onCommand:azdata.resource.deploy",
|
||||
"onCommand:bigDataClusters.command.mount",
|
||||
"onCommand:bigDataClusters.command.refreshmount",
|
||||
"onCommand:bigDataClusters.command.deletemount",
|
||||
"onCommand:bigDataClusters.command.createController",
|
||||
"onCommand:bigDataClusters.command.connectController",
|
||||
"onCommand:bigDataClusters.command.removeController",
|
||||
"onCommand:bigDataClusters.command.manageController",
|
||||
"onCommand:bigDataClusters.command.refreshController",
|
||||
"onView:sqlBigDataCluster"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Microsoft/azuredatastudio.git"
|
||||
},
|
||||
"capabilities": {
|
||||
"virtualWorkspaces": false,
|
||||
"untrustedWorkspaces": {
|
||||
"supported": true
|
||||
}
|
||||
},
|
||||
"main": "./out/extension",
|
||||
"contributes": {
|
||||
"dataExplorer": {
|
||||
"sqlBigDataCluster": [
|
||||
{
|
||||
"id": "sqlBigDataCluster",
|
||||
"name": "%text.sqlServerBigDataClusters%"
|
||||
}
|
||||
]
|
||||
},
|
||||
"menus": {
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "bigDataClusters.command.createController",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "bigDataClusters.command.connectController",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "bigDataClusters.command.removeController",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "bigDataClusters.command.refreshController",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "bigDataClusters.command.manageController",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "bigDataClusters.command.mount",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "bigDataClusters.command.refreshmount",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "bigDataClusters.command.deletemount",
|
||||
"when": "false"
|
||||
}
|
||||
],
|
||||
"view/title": [
|
||||
{
|
||||
"command": "bigDataClusters.command.createController",
|
||||
"when": "view == sqlBigDataCluster",
|
||||
"group": "navigation@1"
|
||||
},
|
||||
{
|
||||
"command": "bigDataClusters.command.connectController",
|
||||
"when": "view == sqlBigDataCluster",
|
||||
"group": "navigation@2"
|
||||
}
|
||||
],
|
||||
"view/item/context": [
|
||||
{
|
||||
"command": "bigDataClusters.command.manageController",
|
||||
"when": "view == sqlBigDataCluster && viewItem == bigDataClusters.itemType.controllerNode",
|
||||
"group": "navigation@1"
|
||||
},
|
||||
{
|
||||
"command": "bigDataClusters.command.refreshController",
|
||||
"when": "view == sqlBigDataCluster && viewItem == bigDataClusters.itemType.controllerNode",
|
||||
"group": "navigation@2"
|
||||
},
|
||||
{
|
||||
"command": "bigDataClusters.command.removeController",
|
||||
"when": "view == sqlBigDataCluster && viewItem == bigDataClusters.itemType.controllerNode",
|
||||
"group": "navigation@3"
|
||||
}
|
||||
],
|
||||
"objectExplorer/item/context": [
|
||||
{
|
||||
"command": "bigDataClusters.command.mount",
|
||||
"when": "nodeType=~/^mssqlCluster/ && nodeType!=mssqlCluster:message && nodeSubType=~/^(?!:mount).*$/",
|
||||
"group": "1mssqlCluster@10"
|
||||
},
|
||||
{
|
||||
"command": "bigDataClusters.command.refreshmount",
|
||||
"when": "nodeType == mssqlCluster:folder && nodeSubType==:mount:",
|
||||
"group": "1mssqlCluster@11"
|
||||
},
|
||||
{
|
||||
"command": "bigDataClusters.command.deletemount",
|
||||
"when": "nodeType == mssqlCluster:folder && nodeSubType==:mount:",
|
||||
"group": "1mssqlCluster@12"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "bigDataClusters.command.createController",
|
||||
"title": "%command.createController.title%",
|
||||
"icon": "$(add)"
|
||||
},
|
||||
{
|
||||
"command": "bigDataClusters.command.connectController",
|
||||
"title": "%command.connectController.title%",
|
||||
"icon": "$(disconnect)"
|
||||
},
|
||||
{
|
||||
"command": "bigDataClusters.command.removeController",
|
||||
"title": "%command.removeController.title%",
|
||||
"when": "viewItem == bigDataClusters.itemType.controllerNode"
|
||||
},
|
||||
{
|
||||
"command": "bigDataClusters.command.refreshController",
|
||||
"title": "%command.refreshController.title%",
|
||||
"icon": "$(refresh)"
|
||||
},
|
||||
{
|
||||
"command": "bigDataClusters.command.manageController",
|
||||
"title": "%command.manageController.title%"
|
||||
},
|
||||
{
|
||||
"command": "bigDataClusters.command.mount",
|
||||
"title": "%command.mount.title%"
|
||||
},
|
||||
{
|
||||
"command": "bigDataClusters.command.refreshmount",
|
||||
"title": "%command.refreshmount.title%"
|
||||
},
|
||||
{
|
||||
"command": "bigDataClusters.command.deletemount",
|
||||
"title": "%command.deletemount.title%"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"type": "object",
|
||||
"title": "%bdc.configuration.title%",
|
||||
"properties": {
|
||||
"bigDataCluster.ignoreSslVerification": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "%bdc.ignoreSslVerification.desc%"
|
||||
}
|
||||
}
|
||||
},
|
||||
"viewsWelcome": [
|
||||
{
|
||||
"view": "sqlBigDataCluster",
|
||||
"contents": "%bdc.view.welcome.connect%",
|
||||
"when": "bdc.loaded"
|
||||
},
|
||||
{
|
||||
"view": "sqlBigDataCluster",
|
||||
"contents": "%bdc.view.welcome.loading%",
|
||||
"when": "!bdc.loaded"
|
||||
}
|
||||
],
|
||||
"resourceDeploymentTypes": [
|
||||
{
|
||||
"name": "sql-bdc",
|
||||
"displayIndex": 3,
|
||||
"displayName": "%resource-type-sql-bdc-display-name%",
|
||||
"description": "%resource-type-sql-bdc-description%",
|
||||
"platforms": "*",
|
||||
"icon": "./images/sql_bdc.svg",
|
||||
"tags": [
|
||||
"On-premises",
|
||||
"SQL Server",
|
||||
"Cloud"
|
||||
],
|
||||
"options": [
|
||||
{
|
||||
"name": "version",
|
||||
"displayName": "%version-display-name%",
|
||||
"values": [
|
||||
{
|
||||
"name": "bdc2019",
|
||||
"displayName": "%bdc-2019-display-name%"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "target",
|
||||
"displayName": "%bdc-deployment-target%",
|
||||
"values": [
|
||||
{
|
||||
"name": "new-aks",
|
||||
"displayName": "%bdc-deployment-target-new-aks%"
|
||||
},
|
||||
{
|
||||
"name": "existing-aks",
|
||||
"displayName": "%bdc-deployment-target-existing-aks%"
|
||||
},
|
||||
{
|
||||
"name": "existing-kubeadm",
|
||||
"displayName": "%bdc-deployment-target-existing-kubeadm%"
|
||||
},
|
||||
{
|
||||
"name": "existing-aro",
|
||||
"displayName": "%bdc-deployment-target-existing-aro%"
|
||||
},
|
||||
{
|
||||
"name": "existing-openshift",
|
||||
"displayName": "%bdc-deployment-target-existing-openshift%"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"providers": [
|
||||
{
|
||||
"name": "sql-bdc_new-aks_bdc2019",
|
||||
"bdcWizard": {
|
||||
"type": "new-aks",
|
||||
"notebook": "./notebooks/deployment/2019/deploy-bdc-aks.ipynb"
|
||||
},
|
||||
"requiredTools": [
|
||||
{
|
||||
"name": "kubectl",
|
||||
"version": "1.13.0"
|
||||
},
|
||||
{
|
||||
"name": "azure-cli"
|
||||
},
|
||||
{
|
||||
"name": "azdata",
|
||||
"version": "20.3.9"
|
||||
}
|
||||
],
|
||||
"when": "target=new-aks&&version=bdc2019"
|
||||
},
|
||||
{
|
||||
"name": "sql-bdc_existing-aks_bdc2019",
|
||||
"bdcWizard": {
|
||||
"type": "existing-aks",
|
||||
"notebook": "./notebooks/deployment/2019/deploy-bdc-existing-aks.ipynb"
|
||||
},
|
||||
"requiredTools": [
|
||||
{
|
||||
"name": "kubectl",
|
||||
"version": "1.13.0"
|
||||
},
|
||||
{
|
||||
"name": "azdata",
|
||||
"version": "20.3.9"
|
||||
}
|
||||
],
|
||||
"when": "target=existing-aks&&version=bdc2019"
|
||||
},
|
||||
{
|
||||
"name": "sql-bdc_existing-kubeadm_bdc2019",
|
||||
"bdcWizard": {
|
||||
"type": "existing-kubeadm",
|
||||
"notebook": "./notebooks/deployment/2019/deploy-bdc-existing-kubeadm.ipynb"
|
||||
},
|
||||
"requiredTools": [
|
||||
{
|
||||
"name": "kubectl",
|
||||
"version": "1.13.0"
|
||||
},
|
||||
{
|
||||
"name": "azdata",
|
||||
"version": "20.3.9"
|
||||
}
|
||||
],
|
||||
"when": "target=existing-kubeadm&&version=bdc2019"
|
||||
},
|
||||
{
|
||||
"name": "sql-bdc_existing-aro_bdc2019",
|
||||
"bdcWizard": {
|
||||
"type": "existing-aro",
|
||||
"notebook": "./notebooks/deployment/2019/deploy-bdc-existing-aro.ipynb"
|
||||
},
|
||||
"requiredTools": [
|
||||
{
|
||||
"name": "kubectl",
|
||||
"version": "1.13.0"
|
||||
},
|
||||
{
|
||||
"name": "azdata",
|
||||
"version": "20.3.9"
|
||||
}
|
||||
],
|
||||
"when": "target=existing-aro&&version=bdc2019"
|
||||
},
|
||||
{
|
||||
"name": "sql-bdc_existing-openshift_bdc2019",
|
||||
"bdcWizard": {
|
||||
"type": "existing-openshift",
|
||||
"notebook": "./notebooks/deployment/2019/deploy-bdc-existing-openshift.ipynb"
|
||||
},
|
||||
"requiredTools": [
|
||||
{
|
||||
"name": "kubectl",
|
||||
"version": "1.13.0"
|
||||
},
|
||||
{
|
||||
"name": "azdata",
|
||||
"version": "20.3.9"
|
||||
}
|
||||
],
|
||||
"when": "target=existing-openshift&&version=bdc2019"
|
||||
}
|
||||
],
|
||||
"agreements": [
|
||||
{
|
||||
"template": "%bdc-agreement%",
|
||||
"links": [
|
||||
{
|
||||
"text": "%microsoft-privacy-statement%",
|
||||
"url": "https://go.microsoft.com/fwlink/?LinkId=853010"
|
||||
},
|
||||
{
|
||||
"text": "%bdc-agreement-bdc-eula%",
|
||||
"url": "https://go.microsoft.com/fwlink/?LinkId=2002534"
|
||||
},
|
||||
{
|
||||
"text": "%bdc-agreement-azdata-eula%",
|
||||
"url": "https://aka.ms/eula-azdata-en"
|
||||
}
|
||||
],
|
||||
"when": "true"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/ads-kerberos": "^1.1.3",
|
||||
"request": "^2.88.0",
|
||||
"vscode-nls": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/request": "^2.48.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"json-schema": "0.4.0"
|
||||
}
|
||||
}
|
||||
46
extensions/big-data-cluster/package.nls.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"description": "Support for managing SQL Server Big Data Clusters",
|
||||
"text.sqlServerBigDataClusters": "SQL Server Big Data Clusters",
|
||||
"command.connectController.title": "Connect to Existing Controller",
|
||||
"command.createController.title": "Create New Controller",
|
||||
"command.removeController.title": "Remove Controller",
|
||||
"command.refreshController.title": "Refresh",
|
||||
"command.manageController.title": "Manage",
|
||||
"command.mount.title": "Mount HDFS",
|
||||
"command.refreshmount.title": "Refresh Mount",
|
||||
"command.deletemount.title": "Delete Mount",
|
||||
"bdc.configuration.title": "Big Data Cluster",
|
||||
"bdc.view.welcome.connect": "No SQL Big Data Cluster controllers registered. [Learn More](https://docs.microsoft.com/sql/big-data-cluster/big-data-cluster-overview)\n[Connect Controller](command:bigDataClusters.command.connectController)",
|
||||
"bdc.view.welcome.loading": "Loading controllers...",
|
||||
"bdc.ignoreSslVerification.desc": "Ignore SSL verification errors against SQL Server Big Data Cluster endpoints such as HDFS, Spark, and Controller if true",
|
||||
"resource-type-sql-bdc-display-name": "SQL Server Big Data Cluster",
|
||||
"resource-type-sql-bdc-description": "SQL Server Big Data Cluster allows you to deploy scalable clusters of SQL Server, Spark, and HDFS containers running on Kubernetes",
|
||||
"version-display-name": "Version",
|
||||
"bdc-2019-display-name": "SQL Server 2019",
|
||||
"bdc-deployment-target": "Deployment target",
|
||||
"bdc-deployment-target-new-aks": "New Azure Kubernetes Service Cluster",
|
||||
"bdc-deployment-target-existing-aks": "Existing Azure Kubernetes Service Cluster",
|
||||
"bdc-deployment-target-existing-kubeadm": "Existing Kubernetes Cluster (kubeadm)",
|
||||
"bdc-deployment-target-existing-aro": "Existing Azure Red Hat OpenShift cluster",
|
||||
"bdc-deployment-target-existing-openshift": "Existing OpenShift cluster",
|
||||
"bdc-cluster-settings-section-title": "SQL Server Big Data Cluster settings",
|
||||
"bdc-cluster-name-field": "Cluster name",
|
||||
"bdc-controller-username-field": "Controller username",
|
||||
"bdc-password-field": "Password",
|
||||
"bdc-confirm-password-field": "Confirm password",
|
||||
"bdc-azure-settings-section-title": "Azure settings",
|
||||
"bdc-azure-subscription-id-field": "Subscription id",
|
||||
"bdc-azure-subscription-id-placeholder": "Use my default Azure subscription",
|
||||
"bdc-azure-resource-group-field": "Resource group name",
|
||||
"bdc-azure-region-field": "Region",
|
||||
"bdc-azure-aks-name-field": "AKS cluster name",
|
||||
"bdc-azure-vm-size-field": "VM size",
|
||||
"bdc-azure-vm-count-field": "VM count",
|
||||
"bdc-storage-class-field": "Storage class name",
|
||||
"bdc-data-size-field": "Capacity for data (GB)",
|
||||
"bdc-log-size-field": "Capacity for logs (GB)",
|
||||
"bdc-agreement": "I accept {0}, {1} and {2}.",
|
||||
"microsoft-privacy-statement": "Microsoft Privacy Statement",
|
||||
"bdc-agreement-azdata-eula": "azdata License Terms",
|
||||
"bdc-agreement-bdc-eula": "SQL Server License Terms"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><defs><style>.cls-1{opacity:0;}.cls-2{fill:#f6f6f6;}.cls-3{fill:#424242;}.cls-4{fill:#f0eff1;}</style></defs><title>centralmanagement_server_16x</title><g id="canvas" class="cls-1"><rect class="cls-2" width="16" height="16"/></g><path id="outline" class="cls-2" d="M16,0H7V1H3V5H0V16H8V13h8ZM6,4H7V5H6Z"/><g id="iconBG"><path class="cls-3" d="M8,1V12h7V1Zm6,10H13V10h1Zm0-6H9V4h5Zm0-2H9V2h5Z"/><path class="cls-3" d="M1,6v9H7V6Zm5,8H5V13H6Zm0-4H2V9H6ZM6,8H2V7H6Z"/><rect class="cls-3" x="6" y="2" width="1" height="1"/><rect class="cls-3" x="4" y="4" width="1" height="1"/><rect class="cls-3" x="4" y="2" width="1" height="1"/></g><g id="iconFG"><path class="cls-4" d="M14,3H9V2h5Zm0,1H9V5h5Zm0,6H13v1h1Z"/><path class="cls-4" d="M6,8H2V7H6ZM6,9H2v1H6Zm0,4H5v1H6Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 869 B |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#fff;}</style></defs><title>copy_inverse</title><path class="cls-1" d="M3,0V3.36H0V16.1H13V12.73h3V0Zm9,15.19H1.08V4.27H12Zm3-3.36H13V3.36H4V.9H15Z"/></svg>
|
||||
|
After Width: | Height: | Size: 274 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 6H11V3H5V6ZM6 4H10V5H6V4Z" fill="white"/>
|
||||
<path d="M1 0H14V16H1V13H0V12H1V10.023H0V9.023H1V7.039H0V6.039H1V4H0V3H1V0ZM13 15V1H2V3H3V4H2V6.039H3V7.039H2V9.023H3V10.023H2V12H3V13H2V15H13Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 317 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M13.451 5.609l-.579-.939-1.068.812-.076.094c-.335.415-.927 1.341-1.124 2.876l-.021.165.033.163.071.345c0 1.654-1.346 3-3 3-.795 0-1.545-.311-2.107-.868-.563-.567-.873-1.317-.873-2.111 0-1.431 1.007-2.632 2.351-2.929v2.926s2.528-2.087 2.984-2.461h.012l3.061-2.582-4.919-4.1h-1.137v2.404c-3.429.318-6.121 3.211-6.121 6.721 0 1.809.707 3.508 1.986 4.782 1.277 1.282 2.976 1.988 4.784 1.988 3.722 0 6.75-3.028 6.75-6.75 0-1.245-.349-2.468-1.007-3.536z" fill="#2D2D30"/><path d="M12.6 6.134l-.094.071c-.269.333-.746 1.096-.91 2.375.057.277.092.495.092.545 0 2.206-1.794 4-4 4-1.098 0-2.093-.445-2.817-1.164-.718-.724-1.163-1.718-1.163-2.815 0-2.206 1.794-4 4-4l.351.025v1.85s1.626-1.342 1.631-1.339l1.869-1.577-3.5-2.917v2.218l-.371-.03c-3.176 0-5.75 2.574-5.75 5.75 0 1.593.648 3.034 1.695 4.076 1.042 1.046 2.482 1.694 4.076 1.694 3.176 0 5.75-2.574 5.75-5.75-.001-1.106-.318-2.135-.859-3.012z" fill="#C5C5C5"/></svg>
|
||||
|
After Width: | Height: | Size: 986 B |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#3bb44a;}</style></defs><title>success_16x16</title><path class="cls-1" d="M16,3.16,5.48,13.69,0,8.2l.89-.89,4.6,4.59,9.63-9.62Z"/></svg>
|
||||
|
After Width: | Height: | Size: 255 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.12 13.9725L15 12.5L9.37927 2H7.61924L1.9985 12.5L2.87852 13.9725H14.12ZM2.87852 12.9725L8.49925 2.47249L14.12 12.9725H2.87852ZM7.98952 6H8.98802V10H7.98952V6ZM7.98952 11H8.98802V12H7.98952V11Z" fill="#FFCC00"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 367 B |
@@ -0,0 +1 @@
|
||||
<svg id="Icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><defs><style>.cls-1{opacity:0;}.cls-2{fill:#f6f6f6;}.cls-3{fill:#424242;}.cls-4{fill:#f0eff1;}</style></defs><title>centralmanagement_server_16x</title><g id="canvas" class="cls-1"><rect class="cls-2" width="16" height="16"/></g><path id="outline" class="cls-2" d="M16,0H7V1H3V5H0V16H8V13h8ZM6,4H7V5H6Z"/><g id="iconBG"><path class="cls-3" d="M8,1V12h7V1Zm6,10H13V10h1Zm0-6H9V4h5Zm0-2H9V2h5Z"/><path class="cls-3" d="M1,6v9H7V6Zm5,8H5V13H6Zm0-4H2V9H6ZM6,8H2V7H6Z"/><rect class="cls-3" x="6" y="2" width="1" height="1"/><rect class="cls-3" x="4" y="4" width="1" height="1"/><rect class="cls-3" x="4" y="2" width="1" height="1"/></g><g id="iconFG"><path class="cls-4" d="M14,3H9V2h5Zm0,1H9V5h5Zm0,6H13v1h1Z"/><path class="cls-4" d="M6,8H2V7H6ZM6,9H2v1H6Zm0,4H5v1H6Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 869 B |
1
extensions/big-data-cluster/resources/light/copy.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>copy</title><path d="M3-.15V3.21H0V15.95H13V12.58h3V-.15ZM12,15H1.08V4.12H12Zm3-3.36H13V3.21h-9V.75H15Z"/></svg>
|
||||
|
After Width: | Height: | Size: 212 B |
7
extensions/big-data-cluster/resources/light/notebook.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<title>Artboard 20</title>
|
||||
<g>
|
||||
<path d="M5,6h6V3H5ZM6,4h4V5H6Z"/>
|
||||
<path d="M1,0H14V16H1V13H0V12H1V10.023H0v-1H1V7.039H0v-1H1V4H0V3H1ZM13,15V1H2V3H3V4H2V6.039H3v1H2V9.023H3v1H2V12H3v1H2v2Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 301 B |
1
extensions/big-data-cluster/resources/light/refresh.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M13.451 5.609l-.579-.939-1.068.812-.076.094c-.335.415-.927 1.341-1.124 2.876l-.021.165.033.163.071.345c0 1.654-1.346 3-3 3-.795 0-1.545-.311-2.107-.868-.563-.567-.873-1.317-.873-2.111 0-1.431 1.007-2.632 2.351-2.929v2.926s2.528-2.087 2.984-2.461h.012l3.061-2.582-4.919-4.1h-1.137v2.404c-3.429.318-6.121 3.211-6.121 6.721 0 1.809.707 3.508 1.986 4.782 1.277 1.282 2.976 1.988 4.784 1.988 3.722 0 6.75-3.028 6.75-6.75 0-1.245-.349-2.468-1.007-3.536z" fill="#F6F6F6"/><path d="M12.6 6.134l-.094.071c-.269.333-.746 1.096-.91 2.375.057.277.092.495.092.545 0 2.206-1.794 4-4 4-1.098 0-2.093-.445-2.817-1.164-.718-.724-1.163-1.718-1.163-2.815 0-2.206 1.794-4 4-4l.351.025v1.85s1.626-1.342 1.631-1.339l1.869-1.577-3.5-2.917v2.218l-.371-.03c-3.176 0-5.75 2.574-5.75 5.75 0 1.593.648 3.034 1.695 4.076 1.042 1.046 2.482 1.694 4.076 1.694 3.176 0 5.75-2.574 5.75-5.75-.001-1.106-.318-2.135-.859-3.012z" fill="#424242"/></svg>
|
||||
|
After Width: | Height: | Size: 986 B |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#3bb44a;}</style></defs><title>success_16x16</title><path class="cls-1" d="M16,3.16,5.48,13.69,0,8.2l.89-.89,4.6,4.59,9.63-9.62Z"/></svg>
|
||||
|
After Width: | Height: | Size: 255 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.12 13.9725L15 12.5L9.37927 2H7.61924L1.9985 12.5L2.87852 13.9725H14.12ZM2.87852 12.9725L8.49925 2.47249L14.12 12.9725H2.87852ZM7.98952 6H8.98802V10H7.98952V6ZM7.98952 11H8.98802V12H7.98952V11Z" fill="#FFCC00"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.12 13.9725L15 12.5L9.37927 2H7.61924L1.9985 12.5L2.87852 13.9725H14.12ZM2.87852 12.9725L8.49925 2.47249L14.12 12.9725H2.87852ZM7.98952 6H8.98802V10H7.98952V6ZM7.98952 11H8.98802V12H7.98952V11Z" fill="#DDB100"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 631 B |
@@ -0,0 +1,2 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048" width="32" height="32">
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 94 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048" width="32" height="32">
|
||||
<path fill="#FF0000" d="M960 256q115 0 221 30t198 84 169 130 130 168 84 199 30 221q0 115-30 221t-84 198-130 169-168 130-199 84-221 30q-115 0-221-30t-198-84-169-130-130-168-84-199-30-221q0-115 30-221t84-198 130-169 168-130 199-84 221-30z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 337 B |
44
extensions/big-data-cluster/src/bdc.d.ts
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
declare module 'bdc' {
|
||||
|
||||
export const enum constants {
|
||||
extensionName = 'Microsoft.big-data-cluster'
|
||||
}
|
||||
|
||||
export interface IExtension {
|
||||
getClusterController(url: string, authType: AuthType, username?: string, password?: string): IClusterController;
|
||||
}
|
||||
|
||||
export interface IEndpointModel {
|
||||
name?: string;
|
||||
description?: string;
|
||||
endpoint?: string;
|
||||
protocol?: string;
|
||||
}
|
||||
|
||||
export interface IHttpResponse {
|
||||
method?: string;
|
||||
url?: string;
|
||||
statusCode?: number;
|
||||
statusMessage?: string;
|
||||
}
|
||||
|
||||
export interface IEndPointsResponse {
|
||||
response: IHttpResponse;
|
||||
endPoints: IEndpointModel[];
|
||||
}
|
||||
|
||||
export type AuthType = 'integrated' | 'basic';
|
||||
|
||||
export interface IClusterController {
|
||||
getClusterConfig(): Promise<any>;
|
||||
getKnoxUsername(defaultUsername: string): Promise<string>;
|
||||
getEndPoints(promptConnect?: boolean): Promise<IEndPointsResponse>
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
}
|
||||
34
extensions/big-data-cluster/src/bigDataCluster/auth.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as kerberos from '@microsoft/ads-kerberos';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export async function authenticateKerberos(hostname: string): Promise<string> {
|
||||
const service = 'HTTP' + (process.platform === 'win32' ? '/' : '@') + hostname;
|
||||
const mechOID = kerberos.GSS_MECH_OID_KRB5;
|
||||
let client = await kerberos.initializeClient(service, { mechOID });
|
||||
let response = await client.step('');
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
type HostAndIp = { host: string, port: string };
|
||||
|
||||
export function getHostAndPortFromEndpoint(endpoint: string): HostAndIp {
|
||||
let authority = vscode.Uri.parse(endpoint).authority;
|
||||
let hostAndPortRegex = /^(.*)([,:](\d+))/g;
|
||||
let match = hostAndPortRegex.exec(authority);
|
||||
if (match) {
|
||||
return {
|
||||
host: match[1],
|
||||
port: match[3]
|
||||
};
|
||||
}
|
||||
return {
|
||||
host: authority,
|
||||
port: undefined
|
||||
};
|
||||
}
|
||||
77
extensions/big-data-cluster/src/bigDataCluster/constants.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
|
||||
export enum BdcItemType {
|
||||
controllerRoot = 'bigDataClusters.itemType.controllerRootNode',
|
||||
controller = 'bigDataClusters.itemType.controllerNode',
|
||||
loadingController = 'bigDataClusters.itemType.loadingControllerNode'
|
||||
}
|
||||
|
||||
export interface IconPath {
|
||||
dark: string;
|
||||
light: string;
|
||||
}
|
||||
|
||||
export class IconPathHelper {
|
||||
private static extensionContext: vscode.ExtensionContext;
|
||||
|
||||
public static controllerNode: IconPath;
|
||||
public static copy: IconPath;
|
||||
public static refresh: IconPath;
|
||||
public static status_ok: IconPath;
|
||||
public static status_warning: IconPath;
|
||||
public static notebook: IconPath;
|
||||
public static status_circle_red: IconPath;
|
||||
public static status_circle_blank: IconPath;
|
||||
|
||||
public static setExtensionContext(extensionContext: vscode.ExtensionContext) {
|
||||
IconPathHelper.extensionContext = extensionContext;
|
||||
IconPathHelper.controllerNode = {
|
||||
dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/bigDataCluster_controller.svg'),
|
||||
light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/bigDataCluster_controller.svg')
|
||||
};
|
||||
IconPathHelper.copy = {
|
||||
light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/copy.svg'),
|
||||
dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/copy_inverse.svg')
|
||||
};
|
||||
IconPathHelper.refresh = {
|
||||
light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/refresh.svg'),
|
||||
dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/refresh_inverse.svg')
|
||||
};
|
||||
IconPathHelper.status_ok = {
|
||||
light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/status_ok_light.svg'),
|
||||
dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/status_ok_dark.svg')
|
||||
};
|
||||
IconPathHelper.status_warning = {
|
||||
light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/status_warning_light.svg'),
|
||||
dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/status_warning_dark.svg')
|
||||
};
|
||||
IconPathHelper.notebook = {
|
||||
light: IconPathHelper.extensionContext.asAbsolutePath('resources/light/notebook.svg'),
|
||||
dark: IconPathHelper.extensionContext.asAbsolutePath('resources/dark/notebook_inverse.svg')
|
||||
};
|
||||
IconPathHelper.status_circle_red = {
|
||||
light: IconPathHelper.extensionContext.asAbsolutePath('resources/status_circle_red.svg'),
|
||||
dark: IconPathHelper.extensionContext.asAbsolutePath('resources/status_circle_red.svg')
|
||||
};
|
||||
IconPathHelper.status_circle_blank = {
|
||||
light: IconPathHelper.extensionContext.asAbsolutePath('resources/status_circle_blank.svg'),
|
||||
dark: IconPathHelper.extensionContext.asAbsolutePath('resources/status_circle_blank.svg')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export namespace cssStyles {
|
||||
export const title = { 'font-size': '14px', 'font-weight': '600' };
|
||||
export const tableHeader = { 'text-align': 'left', 'font-weight': 'bold', 'text-transform': 'uppercase', 'font-size': '10px', 'user-select': 'text' };
|
||||
export const text = { 'margin-block-start': '0px', 'margin-block-end': '0px' };
|
||||
export const lastUpdatedText = { ...text, 'color': '#595959' };
|
||||
export const errorText = { ...text, 'color': 'red' };
|
||||
}
|
||||
|
||||
export const clusterEndpointsProperty = 'clusterEndpoints';
|
||||
export const controllerEndpointName = 'controller';
|
||||
@@ -0,0 +1,455 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as request from 'request';
|
||||
import { authenticateKerberos, getHostAndPortFromEndpoint } from '../auth';
|
||||
import { BdcRouterApi, Authentication, EndpointModel, BdcStatusModel, DefaultApi } from './apiGenerated';
|
||||
import { TokenRouterApi } from './clusterApiGenerated2';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { ConnectControllerDialog, ConnectControllerModel } from '../dialog/connectControllerDialog';
|
||||
import { getIgnoreSslVerificationConfigSetting } from '../utils';
|
||||
import { IClusterController, AuthType, IEndPointsResponse, IHttpResponse } from 'bdc';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
const DEFAULT_KNOX_USERNAME = 'root';
|
||||
|
||||
class SslAuth implements Authentication {
|
||||
constructor() { }
|
||||
|
||||
applyToRequest(requestOptions: request.Options): void {
|
||||
requestOptions.rejectUnauthorized = !getIgnoreSslVerificationConfigSetting();
|
||||
}
|
||||
}
|
||||
|
||||
export class KerberosAuth extends SslAuth implements Authentication {
|
||||
|
||||
constructor(public kerberosToken: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
override applyToRequest(requestOptions: request.Options): void {
|
||||
super.applyToRequest(requestOptions);
|
||||
if (requestOptions && requestOptions.headers) {
|
||||
requestOptions.headers['Authorization'] = `Negotiate ${this.kerberosToken}`;
|
||||
}
|
||||
requestOptions.auth = undefined;
|
||||
}
|
||||
}
|
||||
export class BasicAuth extends SslAuth implements Authentication {
|
||||
constructor(public username: string, public password: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
override applyToRequest(requestOptions: request.Options): void {
|
||||
super.applyToRequest(requestOptions);
|
||||
requestOptions.auth = {
|
||||
username: this.username, password: this.password
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class OAuthWithSsl extends SslAuth implements Authentication {
|
||||
public accessToken: string = '';
|
||||
|
||||
override applyToRequest(requestOptions: request.Options): void {
|
||||
super.applyToRequest(requestOptions);
|
||||
if (requestOptions && requestOptions.headers) {
|
||||
requestOptions.headers['Authorization'] = `Bearer ${this.accessToken}`;
|
||||
}
|
||||
requestOptions.auth = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
class BdcApiWrapper extends BdcRouterApi {
|
||||
constructor(basePathOrUsername: string, password: string, basePath: string, auth: Authentication) {
|
||||
if (password) {
|
||||
super(basePathOrUsername, password, basePath);
|
||||
} else {
|
||||
super(basePath, undefined, undefined);
|
||||
}
|
||||
this.authentications.default = auth;
|
||||
}
|
||||
}
|
||||
class DefaultApiWrapper extends DefaultApi {
|
||||
constructor(basePathOrUsername: string, password: string, basePath: string, auth: Authentication) {
|
||||
if (password) {
|
||||
super(basePathOrUsername, password, basePath);
|
||||
} else {
|
||||
super(basePath, undefined, undefined);
|
||||
}
|
||||
this.authentications.default = auth;
|
||||
}
|
||||
}
|
||||
|
||||
export class ClusterController implements IClusterController {
|
||||
|
||||
private _authPromise: Promise<Authentication>;
|
||||
private _url: string;
|
||||
private readonly _dialog: ConnectControllerDialog;
|
||||
private _connectionPromise: Promise<ClusterController>;
|
||||
|
||||
constructor(url: string,
|
||||
private _authType: AuthType,
|
||||
private _username?: string,
|
||||
private _password?: string
|
||||
) {
|
||||
if (!url || (_authType === 'basic' && (!_username || !_password))) {
|
||||
throw new Error('Missing required inputs for Cluster controller API (URL, username, password)');
|
||||
}
|
||||
this._url = adjustUrl(url);
|
||||
if (this._authType === 'basic') {
|
||||
this._authPromise = Promise.resolve(new BasicAuth(_username, _password));
|
||||
} else {
|
||||
this._authPromise = this.requestTokenUsingKerberos();
|
||||
}
|
||||
this._dialog = new ConnectControllerDialog(new ConnectControllerModel(
|
||||
{
|
||||
url: this._url,
|
||||
auth: this._authType,
|
||||
username: this._username,
|
||||
password: this._password
|
||||
}));
|
||||
}
|
||||
|
||||
public get url(): string {
|
||||
return this._url;
|
||||
}
|
||||
|
||||
public get authType(): AuthType {
|
||||
return this._authType;
|
||||
}
|
||||
|
||||
public get username(): string | undefined {
|
||||
return this._username;
|
||||
}
|
||||
|
||||
public get password(): string | undefined {
|
||||
return this._password;
|
||||
}
|
||||
|
||||
private async requestTokenUsingKerberos(): Promise<Authentication> {
|
||||
let supportsKerberos = await this.verifyKerberosSupported();
|
||||
if (!supportsKerberos) {
|
||||
throw new Error(localize('error.no.activedirectory', "This cluster does not support Windows authentication"));
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
// AD auth is available, login to keberos and convert to token auth for all future calls
|
||||
let host = getHostAndPortFromEndpoint(this._url).host;
|
||||
let kerberosToken = await authenticateKerberos(host);
|
||||
let tokenApi = new TokenRouterApi(this._url);
|
||||
tokenApi.setDefaultAuthentication(new KerberosAuth(kerberosToken));
|
||||
let result = await tokenApi.apiV1TokenPost();
|
||||
let auth = new OAuthWithSsl();
|
||||
auth.accessToken = result.body.accessToken;
|
||||
return auth;
|
||||
} catch (error) {
|
||||
let controllerErr = new ControllerError(error, localize('bdc.error.tokenPost', "Error during authentication"));
|
||||
if (controllerErr.code === 401) {
|
||||
throw new Error(localize('bdc.error.unauthorized', "You do not have permission to log into this cluster using Windows Authentication"));
|
||||
}
|
||||
// Else throw the error as-is
|
||||
throw controllerErr;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that this cluster supports Kerberos authentication. It does this by sending a request to the Token API route
|
||||
* without any credentials and verifying that it gets a 401 response back with a Negotiate www-authenticate header.
|
||||
*/
|
||||
private async verifyKerberosSupported(): Promise<boolean> {
|
||||
let tokenApi = new TokenRouterApi(this._url);
|
||||
tokenApi.setDefaultAuthentication(new SslAuth());
|
||||
try {
|
||||
await tokenApi.apiV1TokenPost();
|
||||
console.warn(`Token API returned success without any auth while verifying Kerberos support for BDC Cluster ${this._url}`);
|
||||
// If we get to here, the route for tokens doesn't require auth which is an unexpected error state
|
||||
return false;
|
||||
}
|
||||
catch (error) {
|
||||
if (!error.response) {
|
||||
console.warn(`No response when verifying Kerberos support for BDC Cluster ${this._url} - ${error}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (error.response.statusCode !== 401) {
|
||||
console.warn(`Got unexpected status code ${error.response.statusCode} when verifying Kerberos support for BDC Cluster ${this._url}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const auths = error.response.headers['www-authenticate'] as string[] ?? [];
|
||||
if (auths.includes('Negotiate')) {
|
||||
return true;
|
||||
}
|
||||
console.warn(`Didn't get expected Negotiate auth type when verifying Kerberos support for BDC Cluster ${this.url}. Supported types : ${auths.join(', ')}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async getKnoxUsername(defaultUsername: string): Promise<string> {
|
||||
// This all is necessary because prior to CU5 BDC deployments all had the same default username for
|
||||
// accessing the Knox gateway. But in the allowRunAsRoot setting was added and defaulted to false - so
|
||||
// if that exists and is false then we use the username instead.
|
||||
// Note that the SQL username may not necessarily be correct here either - but currently this is what
|
||||
// we're requiring to run Notebooks in a BDC
|
||||
const config = await this.getClusterConfig();
|
||||
return config.spec?.spec?.security?.allowRunAsRoot === false ? defaultUsername : DEFAULT_KNOX_USERNAME;
|
||||
}
|
||||
|
||||
public async getClusterConfig(promptConnect: boolean = false): Promise<any> {
|
||||
return await this.withConnectRetry<any>(
|
||||
this.getClusterConfigImpl,
|
||||
promptConnect,
|
||||
localize('bdc.error.getClusterConfig', "Error retrieving cluster config from {0}", this._url));
|
||||
}
|
||||
|
||||
private async getClusterConfigImpl(self: ClusterController): Promise<any> {
|
||||
let auth = await self._authPromise;
|
||||
let endPointApi = new BdcApiWrapper(self._username, self._password, self._url, auth);
|
||||
let options: any = {};
|
||||
|
||||
let result = await endPointApi.getCluster(options);
|
||||
return {
|
||||
response: result.response as IHttpResponse,
|
||||
spec: JSON.parse(result.body.spec)
|
||||
};
|
||||
}
|
||||
|
||||
public async getEndPoints(promptConnect: boolean = false): Promise<IEndPointsResponse> {
|
||||
return await this.withConnectRetry<IEndPointsResponse>(
|
||||
this.getEndpointsImpl,
|
||||
promptConnect,
|
||||
localize('bdc.error.getEndPoints', "Error retrieving endpoints from {0}", this._url));
|
||||
}
|
||||
|
||||
private async getEndpointsImpl(self: ClusterController): Promise<IEndPointsResponse> {
|
||||
let auth = await self._authPromise;
|
||||
let endPointApi = new BdcApiWrapper(self._username, self._password, self._url, auth);
|
||||
let options: any = {};
|
||||
|
||||
let result = await endPointApi.endpointsGet(options);
|
||||
return {
|
||||
response: result.response as IHttpResponse,
|
||||
endPoints: result.body as EndpointModel[]
|
||||
};
|
||||
}
|
||||
|
||||
public async getBdcStatus(promptConnect: boolean = false): Promise<IBdcStatusResponse> {
|
||||
return await this.withConnectRetry<IBdcStatusResponse>(
|
||||
this.getBdcStatusImpl,
|
||||
promptConnect,
|
||||
localize('bdc.error.getBdcStatus', "Error retrieving BDC status from {0}", this._url));
|
||||
}
|
||||
|
||||
private async getBdcStatusImpl(self: ClusterController): Promise<IBdcStatusResponse> {
|
||||
let auth = await self._authPromise;
|
||||
const bdcApi = new BdcApiWrapper(self._username, self._password, self._url, auth);
|
||||
|
||||
const bdcStatus = await bdcApi.getBdcStatus('', '', /*all*/ true);
|
||||
return {
|
||||
response: bdcStatus.response,
|
||||
bdcStatus: bdcStatus.body
|
||||
};
|
||||
}
|
||||
|
||||
public async mountHdfs(mountPath: string, remoteUri: string, credentials: {}, promptConnection: boolean = false): Promise<MountResponse> {
|
||||
return await this.withConnectRetry<MountResponse>(
|
||||
this.mountHdfsImpl,
|
||||
promptConnection,
|
||||
localize('bdc.error.mountHdfs', "Error creating mount"),
|
||||
mountPath,
|
||||
remoteUri,
|
||||
credentials);
|
||||
}
|
||||
|
||||
private async mountHdfsImpl(self: ClusterController, mountPath: string, remoteUri: string, credentials: {}): Promise<MountResponse> {
|
||||
let auth = await self._authPromise;
|
||||
const api = new DefaultApiWrapper(self._username, self._password, self._url, auth);
|
||||
|
||||
const mountStatus = await api.createMount('', '', remoteUri, mountPath, credentials);
|
||||
return {
|
||||
response: mountStatus.response,
|
||||
status: mountStatus.body
|
||||
};
|
||||
}
|
||||
|
||||
public async getMountStatus(mountPath?: string, promptConnect: boolean = false): Promise<MountStatusResponse> {
|
||||
return await this.withConnectRetry<MountStatusResponse>(
|
||||
this.getMountStatusImpl,
|
||||
promptConnect,
|
||||
localize('bdc.error.statusHdfs', "Error getting mount status"),
|
||||
mountPath);
|
||||
}
|
||||
|
||||
private async getMountStatusImpl(self: ClusterController, mountPath?: string): Promise<MountStatusResponse> {
|
||||
const auth = await self._authPromise;
|
||||
const api = new DefaultApiWrapper(self._username, self._password, self._url, auth);
|
||||
|
||||
const mountStatus = await api.listMounts('', '', mountPath);
|
||||
return {
|
||||
response: mountStatus.response,
|
||||
mount: mountStatus.body ? JSON.parse(mountStatus.body) : undefined
|
||||
};
|
||||
}
|
||||
|
||||
public async refreshMount(mountPath: string, promptConnect: boolean = false): Promise<MountResponse> {
|
||||
return await this.withConnectRetry<MountResponse>(
|
||||
this.refreshMountImpl,
|
||||
promptConnect,
|
||||
localize('bdc.error.refreshHdfs', "Error refreshing mount"),
|
||||
mountPath);
|
||||
}
|
||||
|
||||
private async refreshMountImpl(self: ClusterController, mountPath: string): Promise<MountResponse> {
|
||||
const auth = await self._authPromise;
|
||||
const api = new DefaultApiWrapper(self._username, self._password, self._url, auth);
|
||||
|
||||
const mountStatus = await api.refreshMount('', '', mountPath);
|
||||
return {
|
||||
response: mountStatus.response,
|
||||
status: mountStatus.body
|
||||
};
|
||||
}
|
||||
|
||||
public async deleteMount(mountPath: string, promptConnect: boolean = false): Promise<MountResponse> {
|
||||
return await this.withConnectRetry<MountResponse>(
|
||||
this.deleteMountImpl,
|
||||
promptConnect,
|
||||
localize('bdc.error.deleteHdfs', "Error deleting mount"),
|
||||
mountPath);
|
||||
}
|
||||
|
||||
private async deleteMountImpl(self: ClusterController, mountPath: string): Promise<MountResponse> {
|
||||
let auth = await self._authPromise;
|
||||
const api = new DefaultApiWrapper(self._username, self._password, self._url, auth);
|
||||
|
||||
const mountStatus = await api.deleteMount('', '', mountPath);
|
||||
return {
|
||||
response: mountStatus.response,
|
||||
status: mountStatus.body
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that wraps a function call in a try/catch and if promptConnect is true
|
||||
* will prompt the user to re-enter connection information and if that succeeds updates
|
||||
* this with the new information.
|
||||
* @param f The API function we're wrapping
|
||||
* @param promptConnect Whether to actually prompt for connection on failure
|
||||
* @param errorMessage The message to include in the wrapped error thrown
|
||||
* @param args The args to pass to the function
|
||||
*/
|
||||
private async withConnectRetry<T>(f: (...args: any[]) => Promise<T>, promptConnect: boolean, errorMessage: string, ...args: any[]): Promise<T> {
|
||||
try {
|
||||
try {
|
||||
return await f(this, ...args);
|
||||
} catch (error) {
|
||||
if (promptConnect) {
|
||||
// We don't want to open multiple dialogs here if multiple calls come in the same time so check
|
||||
// and see if we have are actively waiting on an open dialog to return and if so then just wait
|
||||
// on that promise.
|
||||
if (!this._connectionPromise) {
|
||||
this._connectionPromise = this._dialog.showDialog();
|
||||
}
|
||||
const controller = await this._connectionPromise;
|
||||
if (controller) {
|
||||
this._username = controller._username;
|
||||
this._password = controller._password;
|
||||
this._url = controller._url;
|
||||
this._authType = controller._authType;
|
||||
this._authPromise = controller._authPromise;
|
||||
}
|
||||
return await f(this, args);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
throw new ControllerError(error, errorMessage);
|
||||
} finally {
|
||||
this._connectionPromise = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes missing protocol and wrong character for port entered by user
|
||||
*/
|
||||
function adjustUrl(url: string): string {
|
||||
if (!url) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
url = url.trim().replace(/ /g, '').replace(/,(\d+)$/, ':$1');
|
||||
if (!url.includes('://')) {
|
||||
url = `https://${url}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
export interface IClusterRequest {
|
||||
url: string;
|
||||
username: string;
|
||||
password?: string;
|
||||
method?: string;
|
||||
}
|
||||
|
||||
export interface IBdcStatusResponse {
|
||||
response: IHttpResponse;
|
||||
bdcStatus: BdcStatusModel;
|
||||
}
|
||||
|
||||
export enum MountState {
|
||||
Creating = 'Creating',
|
||||
Ready = 'Ready',
|
||||
Error = 'Error'
|
||||
}
|
||||
|
||||
export interface MountInfo {
|
||||
mount: string;
|
||||
remote: string;
|
||||
state: MountState;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface MountResponse {
|
||||
response: IHttpResponse;
|
||||
status: any;
|
||||
}
|
||||
export interface MountStatusResponse {
|
||||
response: IHttpResponse;
|
||||
mount: MountInfo[];
|
||||
}
|
||||
|
||||
export class ControllerError extends Error {
|
||||
public code?: number;
|
||||
public reason?: string;
|
||||
public address?: string;
|
||||
public statusMessage?: string;
|
||||
/**
|
||||
*
|
||||
* @param error The original error to wrap
|
||||
* @param messagePrefix Optional text to prefix the error message with
|
||||
*/
|
||||
constructor(error: any, messagePrefix?: string) {
|
||||
super(messagePrefix);
|
||||
// Pull out the response information containing details about the failure
|
||||
if (error.response) {
|
||||
this.code = error.response.statusCode;
|
||||
this.message += `${error.response.statusMessage ? ` - ${error.response.statusMessage}` : ''}` || '';
|
||||
this.address = error.response.url || '';
|
||||
this.statusMessage = error.response.statusMessage;
|
||||
}
|
||||
else if (error.message) {
|
||||
this.message += ` - ${error.message}`;
|
||||
}
|
||||
|
||||
// The body message contains more specific information about the failure
|
||||
if (error.body && error.body.reason) {
|
||||
this.message += ` - ${error.body.reason}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { ClusterController, ControllerError } from '../controller/clusterControllerApi';
|
||||
import { ControllerTreeDataProvider } from '../tree/controllerTreeDataProvider';
|
||||
import { BdcDashboardOptions } from './bdcDashboardModel';
|
||||
import { ControllerNode } from '../tree/controllerTreeNode';
|
||||
import { ManageControllerCommand } from '../../commands';
|
||||
import * as loc from '../localizedConstants';
|
||||
import { AuthType } from 'bdc';
|
||||
|
||||
function getAuthCategory(name: AuthType): azdata.CategoryValue {
|
||||
if (name === 'basic') {
|
||||
return { name: name, displayName: loc.basic };
|
||||
}
|
||||
return { name: name, displayName: loc.windowsAuth };
|
||||
}
|
||||
|
||||
export class AddControllerDialogModel {
|
||||
|
||||
private _canceled = false;
|
||||
private _authTypes: azdata.CategoryValue[];
|
||||
constructor(
|
||||
public treeDataProvider: ControllerTreeDataProvider,
|
||||
public node?: ControllerNode,
|
||||
public prefilledUrl?: string,
|
||||
public prefilledAuth?: azdata.CategoryValue,
|
||||
public prefilledUsername?: string,
|
||||
public prefilledPassword?: string,
|
||||
public prefilledRememberPassword?: boolean
|
||||
) {
|
||||
this.prefilledUrl = prefilledUrl || (node && node['url']);
|
||||
this.prefilledAuth = prefilledAuth;
|
||||
if (!prefilledAuth) {
|
||||
let auth = (node && node['auth']) || 'basic';
|
||||
this.prefilledAuth = getAuthCategory(auth);
|
||||
}
|
||||
this.prefilledUsername = prefilledUsername || (node && node['username']);
|
||||
this.prefilledPassword = prefilledPassword || (node && node['password']);
|
||||
this.prefilledRememberPassword = prefilledRememberPassword || (node && node['rememberPassword']);
|
||||
}
|
||||
|
||||
public get authCategories(): azdata.CategoryValue[] {
|
||||
if (!this._authTypes) {
|
||||
this._authTypes = [getAuthCategory('basic'), getAuthCategory('integrated')];
|
||||
}
|
||||
return this._authTypes;
|
||||
}
|
||||
|
||||
public async onComplete(url: string, auth: AuthType, username: string, password: string, rememberPassword: boolean): Promise<void> {
|
||||
try {
|
||||
|
||||
if (auth === 'basic') {
|
||||
// Verify username and password as we can't make them required in the UI
|
||||
if (!username) {
|
||||
throw new Error(loc.usernameRequired);
|
||||
} else if (!password) {
|
||||
throw new Error(loc.passwordRequired);
|
||||
}
|
||||
}
|
||||
// We pre-fetch the endpoints here to verify that the information entered is correct (the user is able to connect)
|
||||
let controller = new ClusterController(url, auth, username, password);
|
||||
let response = await controller.getEndPoints();
|
||||
if (response && response.endPoints) {
|
||||
if (this._canceled) {
|
||||
return;
|
||||
}
|
||||
this.treeDataProvider.addOrUpdateController(url, auth, username, password, rememberPassword);
|
||||
vscode.commands.executeCommand(ManageControllerCommand, <BdcDashboardOptions>{ url: url, auth: auth, username: username, password: password });
|
||||
await this.treeDataProvider.saveControllers();
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore the error if we cancelled the request since we can't stop the actual request from completing
|
||||
if (!this._canceled) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async onError(error: ControllerError): Promise<void> {
|
||||
// implement
|
||||
}
|
||||
|
||||
public async onCancel(): Promise<void> {
|
||||
this._canceled = true;
|
||||
if (this.node) {
|
||||
this.node.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class AddControllerDialog {
|
||||
|
||||
private dialog: azdata.window.Dialog;
|
||||
private uiModelBuilder: azdata.ModelBuilder;
|
||||
|
||||
private urlInputBox: azdata.InputBoxComponent;
|
||||
private authDropdown: azdata.DropDownComponent;
|
||||
private usernameInputBox: azdata.InputBoxComponent;
|
||||
private passwordInputBox: azdata.InputBoxComponent;
|
||||
private rememberPwCheckBox: azdata.CheckBoxComponent;
|
||||
|
||||
constructor(private model: AddControllerDialogModel) {
|
||||
}
|
||||
|
||||
public showDialog(): void {
|
||||
this.createDialog();
|
||||
azdata.window.openDialog(this.dialog);
|
||||
}
|
||||
|
||||
private createDialog(): void {
|
||||
this.dialog = azdata.window.createModelViewDialog(loc.addNewController);
|
||||
this.dialog.registerContent(async view => {
|
||||
this.uiModelBuilder = view.modelBuilder;
|
||||
|
||||
this.urlInputBox = this.uiModelBuilder.inputBox()
|
||||
.withProps({
|
||||
placeHolder: loc.url.toLocaleLowerCase(),
|
||||
value: this.model.prefilledUrl
|
||||
}).component();
|
||||
this.authDropdown = this.uiModelBuilder.dropDown().withProps({
|
||||
values: this.model.authCategories,
|
||||
value: this.model.prefilledAuth,
|
||||
editable: false,
|
||||
}).component();
|
||||
this.authDropdown.onValueChanged(e => this.onAuthChanged());
|
||||
this.usernameInputBox = this.uiModelBuilder.inputBox()
|
||||
.withProps({
|
||||
placeHolder: loc.usernameRequired.toLocaleLowerCase(),
|
||||
value: this.model.prefilledUsername
|
||||
}).component();
|
||||
this.passwordInputBox = this.uiModelBuilder.inputBox()
|
||||
.withProps({
|
||||
placeHolder: loc.password,
|
||||
inputType: 'password',
|
||||
value: this.model.prefilledPassword
|
||||
})
|
||||
.component();
|
||||
this.rememberPwCheckBox = this.uiModelBuilder.checkBox()
|
||||
.withProps({
|
||||
label: loc.rememberPassword,
|
||||
checked: this.model.prefilledRememberPassword
|
||||
}).component();
|
||||
|
||||
let formModel = this.uiModelBuilder.formContainer()
|
||||
.withFormItems([{
|
||||
components: [
|
||||
{
|
||||
component: this.urlInputBox,
|
||||
title: loc.clusterUrl,
|
||||
required: true
|
||||
}, {
|
||||
component: this.authDropdown,
|
||||
title: loc.authType,
|
||||
required: true
|
||||
}, {
|
||||
component: this.usernameInputBox,
|
||||
title: loc.username,
|
||||
required: false
|
||||
}, {
|
||||
component: this.passwordInputBox,
|
||||
title: loc.password,
|
||||
required: false
|
||||
}, {
|
||||
component: this.rememberPwCheckBox,
|
||||
title: ''
|
||||
}
|
||||
],
|
||||
title: ''
|
||||
}]).withLayout({ width: '100%' }).component();
|
||||
this.onAuthChanged();
|
||||
await view.initializeModel(formModel);
|
||||
this.urlInputBox.focus();
|
||||
});
|
||||
|
||||
this.dialog.registerCloseValidator(async () => await this.validate());
|
||||
this.dialog.cancelButton.onClick(async () => await this.cancel());
|
||||
this.dialog.okButton.label = loc.add;
|
||||
this.dialog.cancelButton.label = loc.cancel;
|
||||
}
|
||||
|
||||
private get authValue(): AuthType {
|
||||
return (<azdata.CategoryValue>this.authDropdown.value).name as AuthType;
|
||||
}
|
||||
|
||||
private onAuthChanged(): void {
|
||||
let isBasic = this.authValue === 'basic';
|
||||
this.usernameInputBox.enabled = isBasic;
|
||||
this.passwordInputBox.enabled = isBasic;
|
||||
this.rememberPwCheckBox.enabled = isBasic;
|
||||
if (!isBasic) {
|
||||
this.usernameInputBox.value = '';
|
||||
this.passwordInputBox.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
private async validate(): Promise<boolean> {
|
||||
let url = this.urlInputBox && this.urlInputBox.value;
|
||||
let auth = this.authValue;
|
||||
let username = this.usernameInputBox && this.usernameInputBox.value;
|
||||
let password = this.passwordInputBox && this.passwordInputBox.value;
|
||||
let rememberPassword = this.passwordInputBox && !!this.rememberPwCheckBox.checked;
|
||||
|
||||
try {
|
||||
await this.model.onComplete(url, auth, username, password, rememberPassword);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.dialog.message = {
|
||||
text: (typeof error === 'string') ? error : error.message,
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
if (this.model && this.model.onError) {
|
||||
await this.model.onError(error as ControllerError);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async cancel(): Promise<void> {
|
||||
if (this.model && this.model.onCancel) {
|
||||
await this.model.onCancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { BdcDashboardModel, BdcErrorEvent } from './bdcDashboardModel';
|
||||
import { BdcServiceStatusPage } from './bdcServiceStatusPage';
|
||||
import { BdcDashboardOverviewPage } from './bdcDashboardOverviewPage';
|
||||
import { BdcStatusModel, ServiceStatusModel } from '../controller/apiGenerated';
|
||||
import { getServiceNameDisplayText, showErrorMessage, getHealthStatusDotIcon } from '../utils';
|
||||
import { HdfsDialogCancelledError } from './hdfsDialogBase';
|
||||
import { InitializingComponent } from './intializingComponent';
|
||||
import * as loc from '../localizedConstants';
|
||||
|
||||
export class BdcDashboard extends InitializingComponent {
|
||||
|
||||
private dashboard: azdata.window.ModelViewDashboard;
|
||||
|
||||
private modelView: azdata.ModelView;
|
||||
|
||||
private createdServicePages: Map<string, azdata.DashboardTab> = new Map<string, azdata.DashboardTab>();
|
||||
private overviewTab: azdata.DashboardTab;
|
||||
|
||||
constructor(private title: string, private model: BdcDashboardModel) {
|
||||
super();
|
||||
model.onDidUpdateBdcStatus(bdcStatus => this.eventuallyRunOnInitialized(() => this.handleBdcStatusUpdate(bdcStatus)));
|
||||
model.onBdcError(errorEvent => this.eventuallyRunOnInitialized(() => this.handleError(errorEvent)));
|
||||
}
|
||||
|
||||
public async showDashboard(): Promise<void> {
|
||||
await this.createDashboard();
|
||||
await this.dashboard.open();
|
||||
}
|
||||
|
||||
private async createDashboard(): Promise<void> {
|
||||
this.dashboard = azdata.window.createModelViewDashboard(this.title, 'BdcDashboard', { alwaysShowTabs: true });
|
||||
this.dashboard.registerTabs(async (modelView: azdata.ModelView) => {
|
||||
this.modelView = modelView;
|
||||
|
||||
const overviewPage = new BdcDashboardOverviewPage(this.model, modelView, this.dashboard);
|
||||
this.overviewTab = {
|
||||
title: loc.bdcOverview,
|
||||
id: 'overview-tab',
|
||||
content: overviewPage.container,
|
||||
toolbar: overviewPage.toolbarContainer
|
||||
};
|
||||
return [
|
||||
this.overviewTab
|
||||
];
|
||||
});
|
||||
this.initialized = true;
|
||||
|
||||
// Now that we've created the UI load data from the model in case it already had data
|
||||
this.handleBdcStatusUpdate(this.model.bdcStatus);
|
||||
}
|
||||
|
||||
private handleBdcStatusUpdate(bdcStatus?: BdcStatusModel): void {
|
||||
if (!bdcStatus) {
|
||||
return;
|
||||
}
|
||||
this.updateServicePages(bdcStatus.services);
|
||||
}
|
||||
|
||||
private handleError(errorEvent: BdcErrorEvent): void {
|
||||
if (errorEvent.errorType !== 'general') {
|
||||
return;
|
||||
}
|
||||
// We don't want to show an error for the connection dialog being
|
||||
// canceled since that's a normal case.
|
||||
if (!(errorEvent.error instanceof HdfsDialogCancelledError)) {
|
||||
showErrorMessage(errorEvent.error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the service tab pages, creating any new ones as necessary
|
||||
*/
|
||||
private updateServicePages(services?: ServiceStatusModel[]): void {
|
||||
if (services) {
|
||||
// Create a service page for each new service. We currently don't support services being removed.
|
||||
services.forEach(s => {
|
||||
const existingPage = this.createdServicePages.get(s.serviceName);
|
||||
if (existingPage) {
|
||||
existingPage.icon = getHealthStatusDotIcon(s.healthStatus);
|
||||
} else {
|
||||
const serviceStatusPage = new BdcServiceStatusPage(s.serviceName, this.model, this.modelView);
|
||||
const newTab = <azdata.Tab>{
|
||||
title: getServiceNameDisplayText(s.serviceName),
|
||||
id: s.serviceName,
|
||||
icon: getHealthStatusDotIcon(s.healthStatus),
|
||||
content: serviceStatusPage.container,
|
||||
toolbar: serviceStatusPage.toolbarContainer
|
||||
};
|
||||
this.createdServicePages.set(s.serviceName, newTab);
|
||||
}
|
||||
});
|
||||
this.dashboard.updateTabs([
|
||||
this.overviewTab,
|
||||
{
|
||||
title: loc.clusterDetails,
|
||||
tabs: Array.from(this.createdServicePages.values())
|
||||
}]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { ClusterController } from '../controller/clusterControllerApi';
|
||||
import { EndpointModel, BdcStatusModel } from '../controller/apiGenerated';
|
||||
import { Endpoint, Service } from '../utils';
|
||||
import { ConnectControllerDialog, ConnectControllerModel } from './connectControllerDialog';
|
||||
import { ControllerTreeDataProvider } from '../tree/controllerTreeDataProvider';
|
||||
import { AuthType } from 'bdc';
|
||||
|
||||
export type BdcDashboardOptions = { url: string, auth: AuthType, username: string, password: string, rememberPassword: boolean };
|
||||
|
||||
type BdcErrorType = 'bdcStatus' | 'bdcEndpoints' | 'general';
|
||||
export type BdcErrorEvent = { error: Error, errorType: BdcErrorType };
|
||||
|
||||
export class BdcDashboardModel {
|
||||
|
||||
private _clusterController: ClusterController;
|
||||
private _bdcStatus: BdcStatusModel | undefined;
|
||||
private _endpoints: EndpointModel[] | undefined;
|
||||
private _bdcStatusLastUpdated: Date | undefined;
|
||||
private _endpointsLastUpdated: Date | undefined;
|
||||
private readonly _onDidUpdateEndpoints = new vscode.EventEmitter<EndpointModel[]>();
|
||||
private readonly _onDidUpdateBdcStatus = new vscode.EventEmitter<BdcStatusModel>();
|
||||
private readonly _onBdcError = new vscode.EventEmitter<BdcErrorEvent>();
|
||||
public onDidUpdateEndpoints = this._onDidUpdateEndpoints.event;
|
||||
public onDidUpdateBdcStatus = this._onDidUpdateBdcStatus.event;
|
||||
public onBdcError = this._onBdcError.event;
|
||||
|
||||
constructor(private _options: BdcDashboardOptions, private _treeDataProvider: ControllerTreeDataProvider) {
|
||||
try {
|
||||
this._clusterController = new ClusterController(_options.url, _options.auth, _options.username, _options.password);
|
||||
this.refresh().catch(e => console.log(`Unexpected error refreshing BdcModel ${e instanceof Error ? e.message : e}`));
|
||||
} catch {
|
||||
this.promptReconnect().then(async () => {
|
||||
await this.refresh();
|
||||
}).catch(error => {
|
||||
this._onBdcError.fire({ error: error, errorType: 'general' });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public get bdcStatus(): BdcStatusModel | undefined {
|
||||
return this._bdcStatus;
|
||||
}
|
||||
|
||||
public get serviceEndpoints(): EndpointModel[] | undefined {
|
||||
return this._endpoints;
|
||||
}
|
||||
|
||||
public get bdcStatusLastUpdated(): Date | undefined {
|
||||
return this._bdcStatusLastUpdated;
|
||||
}
|
||||
|
||||
public get endpointsLastUpdated(): Date | undefined {
|
||||
return this._endpointsLastUpdated;
|
||||
}
|
||||
|
||||
public async refresh(): Promise<void> {
|
||||
try {
|
||||
if (!this._clusterController) {
|
||||
// If this succeeds without error we know we have a clusterController at this point
|
||||
await this.promptReconnect();
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
this._clusterController.getBdcStatus(true).then(response => {
|
||||
this._bdcStatus = response.bdcStatus;
|
||||
this._bdcStatusLastUpdated = new Date();
|
||||
this._onDidUpdateBdcStatus.fire(this.bdcStatus);
|
||||
}).catch(error => this._onBdcError.fire({ error: error, errorType: 'bdcStatus' })),
|
||||
this._clusterController.getEndPoints(true).then(response => {
|
||||
this._endpoints = response.endPoints;
|
||||
fixEndpoints(this._endpoints);
|
||||
this._endpointsLastUpdated = new Date();
|
||||
this._onDidUpdateEndpoints.fire(this.serviceEndpoints);
|
||||
}).catch(error => this._onBdcError.fire({ error: error, errorType: 'bdcEndpoints' }))
|
||||
]);
|
||||
} catch (error) {
|
||||
this._onBdcError.fire({ error: error, errorType: 'general' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a partially filled connection profile for the SQL Server Master Instance endpoint
|
||||
* associated with this cluster.
|
||||
* @returns The IConnectionProfile - or undefined if the endpoints haven't been loaded yet
|
||||
*/
|
||||
public getSqlServerMasterConnectionProfile(): azdata.IConnectionProfile | undefined {
|
||||
const sqlServerMasterEndpoint = this.serviceEndpoints && this.serviceEndpoints.find(e => e.name === Endpoint.sqlServerMaster);
|
||||
if (!sqlServerMasterEndpoint) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// We default to sa - if that doesn't work then callers of this should open up a connection
|
||||
// dialog so the user can enter in the correct connection information
|
||||
return {
|
||||
connectionName: undefined,
|
||||
serverName: sqlServerMasterEndpoint.endpoint,
|
||||
databaseName: undefined,
|
||||
userName: 'sa',
|
||||
password: this._options.password,
|
||||
authenticationType: '',
|
||||
savePassword: true,
|
||||
groupFullName: undefined,
|
||||
groupId: undefined,
|
||||
providerName: 'MSSQL',
|
||||
saveProfile: true,
|
||||
id: undefined,
|
||||
options: {}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens up a dialog prompting the user to re-enter credentials for the controller
|
||||
*/
|
||||
private async promptReconnect(): Promise<void> {
|
||||
this._clusterController = await new ConnectControllerDialog(new ConnectControllerModel(this._options)).showDialog();
|
||||
await this.updateController();
|
||||
}
|
||||
|
||||
private async updateController(): Promise<void> {
|
||||
if (!this._clusterController) {
|
||||
return;
|
||||
}
|
||||
this._treeDataProvider.addOrUpdateController(
|
||||
this._clusterController.url,
|
||||
this._clusterController.authType,
|
||||
this._clusterController.username,
|
||||
this._clusterController.password,
|
||||
this._options.rememberPassword);
|
||||
await this._treeDataProvider.saveControllers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the troubleshoot book URL for the specified service, defaulting to the BDC
|
||||
* troubleshoot notebook if the service name is unknown.
|
||||
* @param service The service name to get the troubleshoot notebook URL for
|
||||
*/
|
||||
export function getTroubleshootNotebookUrl(service?: string): string {
|
||||
service = service || '';
|
||||
switch (service.toLowerCase()) {
|
||||
case Service.sql:
|
||||
return 'troubleshooters/tsg101-troubleshoot-sql-server';
|
||||
case Service.hdfs:
|
||||
return 'troubleshooters/tsg102-troubleshoot-hdfs';
|
||||
case Service.spark:
|
||||
return 'troubleshooters/tsg103-troubleshoot-spark';
|
||||
case Service.control:
|
||||
return 'troubleshooters/tsg104-troubleshoot-control';
|
||||
case Service.gateway:
|
||||
return 'troubleshooters/tsg105-troubleshoot-gateway';
|
||||
case Service.app:
|
||||
return 'troubleshooters/tsg106-troubleshoot-app';
|
||||
}
|
||||
return 'troubleshooters/tsg100-troubleshoot-bdc';
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies fixes to the endpoints received so they are displayed correctly
|
||||
* @param endpoints The endpoints received to modify
|
||||
*/
|
||||
function fixEndpoints(endpoints?: EndpointModel[]): void {
|
||||
if (!endpoints) {
|
||||
return;
|
||||
}
|
||||
endpoints.forEach(e => {
|
||||
if (e.name === Endpoint.metricsui && e.endpoint && e.endpoint.indexOf('/d/wZx3OUdmz') === -1) {
|
||||
// Update to have correct URL
|
||||
e.endpoint += '/d/wZx3OUdmz';
|
||||
}
|
||||
if (e.name === Endpoint.logsui && e.endpoint && e.endpoint.indexOf('/app/kibana#/discover') === -1) {
|
||||
// Update to have correct URL
|
||||
e.endpoint += '/app/kibana#/discover';
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,468 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { BdcDashboardModel, BdcErrorEvent } from './bdcDashboardModel';
|
||||
import { IconPathHelper, cssStyles } from '../constants';
|
||||
import { getStateDisplayText, getHealthStatusDisplayText, getEndpointDisplayText, getHealthStatusIcon, getServiceNameDisplayText, Endpoint, getBdcStatusErrorMessage } from '../utils';
|
||||
import { EndpointModel, BdcStatusModel } from '../controller/apiGenerated';
|
||||
import { createViewDetailsButton } from './commonControls';
|
||||
import { HdfsDialogCancelledError } from './hdfsDialogBase';
|
||||
import { BdcDashboardPage } from './bdcDashboardPage';
|
||||
import * as loc from '../localizedConstants';
|
||||
|
||||
const hyperlinkedEndpoints = [Endpoint.metricsui, Endpoint.logsui, Endpoint.sparkHistory, Endpoint.yarnUi];
|
||||
|
||||
export class BdcDashboardOverviewPage extends BdcDashboardPage {
|
||||
private rootContainer: azdata.FlexContainer;
|
||||
private lastUpdatedLabel: azdata.TextComponent;
|
||||
private propertiesContainerLoadingComponent: azdata.LoadingComponent;
|
||||
|
||||
private serviceStatusTable: azdata.DeclarativeTableComponent;
|
||||
private endpointsTable: azdata.DeclarativeTableComponent;
|
||||
private endpointsLoadingComponent: azdata.LoadingComponent;
|
||||
private endpointsDisplayContainer: azdata.FlexContainer;
|
||||
private serviceStatusLoadingComponent: azdata.LoadingComponent;
|
||||
private serviceStatusDisplayContainer: azdata.FlexContainer;
|
||||
private propertiesErrorMessage: azdata.TextComponent;
|
||||
private endpointsErrorMessage: azdata.TextComponent;
|
||||
private serviceStatusErrorMessage: azdata.TextComponent;
|
||||
|
||||
constructor(model: BdcDashboardModel, modelView: azdata.ModelView, private dashboard: azdata.window.ModelViewDashboard) {
|
||||
super(model, modelView);
|
||||
this.model.onDidUpdateEndpoints(endpoints => this.eventuallyRunOnInitialized(() => this.handleEndpointsUpdate(endpoints)));
|
||||
this.model.onDidUpdateBdcStatus(bdcStatus => this.eventuallyRunOnInitialized(() => this.handleBdcStatusUpdate(bdcStatus)));
|
||||
this.model.onBdcError(error => this.eventuallyRunOnInitialized(() => this.handleBdcError(error)));
|
||||
}
|
||||
|
||||
public get container(): azdata.FlexContainer {
|
||||
// Lazily create the container only when needed
|
||||
if (!this.rootContainer) {
|
||||
this.rootContainer = this.createContainer();
|
||||
}
|
||||
return this.rootContainer;
|
||||
}
|
||||
|
||||
public createContainer(): azdata.FlexContainer {
|
||||
const rootContainer = this.modelView.modelBuilder.flexContainer().withLayout(
|
||||
{
|
||||
flexFlow: 'column',
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}).component();
|
||||
|
||||
// ##############
|
||||
// # PROPERTIES #
|
||||
// ##############
|
||||
|
||||
const propertiesLabel = this.modelView.modelBuilder.text()
|
||||
.withProps({ value: loc.clusterProperties, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '10px' } })
|
||||
.component();
|
||||
rootContainer.addItem(propertiesLabel, { CSSStyles: { 'margin-top': '15px', 'padding-left': '10px', ...cssStyles.title } });
|
||||
|
||||
const propertiesContainer = this.modelView.modelBuilder.propertiesContainer().component();
|
||||
|
||||
this.propertiesContainerLoadingComponent = this.modelView.modelBuilder.loadingComponent().withItem(propertiesContainer).component();
|
||||
rootContainer.addItem(this.propertiesContainerLoadingComponent, { flex: '0 0 auto', CSSStyles: { 'padding-left': '10px' } });
|
||||
|
||||
// ############
|
||||
// # OVERVIEW #
|
||||
// ############
|
||||
|
||||
const overviewHeaderContainer = this.modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', height: '20px' }).component();
|
||||
rootContainer.addItem(overviewHeaderContainer, { CSSStyles: { 'padding-left': '10px', 'padding-top': '15px' } });
|
||||
|
||||
const overviewLabel = this.modelView.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.clusterOverview,
|
||||
CSSStyles: { ...cssStyles.text }
|
||||
})
|
||||
.component();
|
||||
|
||||
overviewHeaderContainer.addItem(overviewLabel, { CSSStyles: { ...cssStyles.title } });
|
||||
|
||||
this.lastUpdatedLabel = this.modelView.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.lastUpdated(),
|
||||
CSSStyles: { ...cssStyles.lastUpdatedText }
|
||||
}).component();
|
||||
|
||||
overviewHeaderContainer.addItem(this.lastUpdatedLabel, { CSSStyles: { 'margin-left': '45px' } });
|
||||
|
||||
const overviewContainer = this.modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', width: '100%', height: '100%' }).component();
|
||||
|
||||
this.serviceStatusTable = this.modelView.modelBuilder.declarativeTable()
|
||||
.withProps(
|
||||
{
|
||||
columns: [
|
||||
{ // status icon
|
||||
displayName: '',
|
||||
ariaLabel: loc.statusIcon,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
isReadOnly: true,
|
||||
width: 25,
|
||||
headerCssStyles: {
|
||||
'border': 'none'
|
||||
},
|
||||
rowCssStyles: {
|
||||
'border-top': 'solid 1px #ccc',
|
||||
'border-bottom': 'solid 1px #ccc',
|
||||
'border-left': 'none',
|
||||
'border-right': 'none'
|
||||
},
|
||||
},
|
||||
{ // service
|
||||
displayName: loc.serviceName,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
isReadOnly: true,
|
||||
width: 175,
|
||||
headerCssStyles: {
|
||||
'border': 'none',
|
||||
...cssStyles.tableHeader
|
||||
},
|
||||
rowCssStyles: {
|
||||
'border-top': 'solid 1px #ccc',
|
||||
'border-bottom': 'solid 1px #ccc',
|
||||
'border-left': 'none',
|
||||
'border-right': 'none'
|
||||
},
|
||||
},
|
||||
{ // state
|
||||
displayName: loc.state,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: 150,
|
||||
headerCssStyles: {
|
||||
'border': 'none',
|
||||
...cssStyles.tableHeader
|
||||
},
|
||||
rowCssStyles: {
|
||||
'border-top': 'solid 1px #ccc',
|
||||
'border-bottom': 'solid 1px #ccc',
|
||||
'border-left': 'none',
|
||||
'border-right': 'none'
|
||||
},
|
||||
},
|
||||
{ // health status
|
||||
displayName: loc.healthStatus,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: 100,
|
||||
headerCssStyles: {
|
||||
'border': 'none',
|
||||
'text-align': 'left',
|
||||
...cssStyles.tableHeader
|
||||
},
|
||||
rowCssStyles: {
|
||||
'border-top': 'solid 1px #ccc',
|
||||
'border-bottom': 'solid 1px #ccc',
|
||||
'border-left': 'none',
|
||||
'border-right': 'none'
|
||||
}
|
||||
},
|
||||
{ // view details button
|
||||
displayName: '',
|
||||
ariaLabel: loc.viewErrorDetails,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
isReadOnly: true,
|
||||
width: 150,
|
||||
headerCssStyles: {
|
||||
'border': 'none',
|
||||
},
|
||||
rowCssStyles: {
|
||||
'border-top': 'solid 1px #ccc',
|
||||
'border-bottom': 'solid 1px #ccc',
|
||||
'border-left': 'none',
|
||||
'border-right': 'none'
|
||||
},
|
||||
},
|
||||
],
|
||||
data: [],
|
||||
ariaLabel: loc.clusterOverview
|
||||
})
|
||||
.component();
|
||||
|
||||
this.serviceStatusDisplayContainer = this.modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
|
||||
this.serviceStatusDisplayContainer.addItem(this.serviceStatusTable);
|
||||
|
||||
// Note we don't make the table a child of the loading component since making the loading component align correctly
|
||||
// messes up the layout for the table that we display after loading is finished. Instead we'll just remove the loading
|
||||
// component once it's finished loading the content
|
||||
this.serviceStatusLoadingComponent = this.modelView.modelBuilder.loadingComponent()
|
||||
.withProps({ CSSStyles: { 'padding-top': '0px', 'padding-bottom': '0px' } })
|
||||
.component();
|
||||
|
||||
this.serviceStatusDisplayContainer.addItem(this.serviceStatusLoadingComponent, { flex: '0 0 auto', CSSStyles: { 'padding-left': '150px', width: '30px' } });
|
||||
|
||||
this.serviceStatusErrorMessage = this.modelView.modelBuilder.text().withProps({ display: 'none', CSSStyles: { ...cssStyles.errorText } }).component();
|
||||
overviewContainer.addItem(this.serviceStatusErrorMessage);
|
||||
|
||||
overviewContainer.addItem(this.serviceStatusDisplayContainer);
|
||||
|
||||
rootContainer.addItem(overviewContainer, { flex: '0 0 auto' });
|
||||
|
||||
// #####################
|
||||
// # SERVICE ENDPOINTS #
|
||||
// #####################
|
||||
|
||||
const endpointsLabel = this.modelView.modelBuilder.text()
|
||||
.withProps({ value: loc.serviceEndpoints, CSSStyles: { 'margin-block-start': '20px', 'margin-block-end': '0px' } })
|
||||
.component();
|
||||
rootContainer.addItem(endpointsLabel, { CSSStyles: { 'padding-left': '10px', ...cssStyles.title } });
|
||||
|
||||
this.endpointsErrorMessage = this.modelView.modelBuilder.text().withProps({ display: 'none', CSSStyles: { ...cssStyles.errorText } }).component();
|
||||
|
||||
const endpointsContainer = this.modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'column', width: '100%', height: '100%' }).component();
|
||||
|
||||
this.endpointsTable = this.modelView.modelBuilder.declarativeTable()
|
||||
.withProps(
|
||||
{
|
||||
columns: [
|
||||
{ // service
|
||||
displayName: loc.service,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: 200,
|
||||
headerCssStyles: {
|
||||
'border': 'none',
|
||||
...cssStyles.tableHeader
|
||||
},
|
||||
rowCssStyles: {
|
||||
'border-top': 'solid 1px #ccc',
|
||||
'border-bottom': 'solid 1px #ccc',
|
||||
'border-left': 'none',
|
||||
'border-right': 'none'
|
||||
},
|
||||
},
|
||||
{ // endpoint
|
||||
displayName: loc.endpoint,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
isReadOnly: true,
|
||||
width: 350,
|
||||
headerCssStyles: {
|
||||
'border': 'none',
|
||||
...cssStyles.tableHeader
|
||||
},
|
||||
rowCssStyles: {
|
||||
'border-top': 'solid 1px #ccc',
|
||||
'border-bottom': 'solid 1px #ccc',
|
||||
'border-left': 'none',
|
||||
'border-right': 'none',
|
||||
'overflow': 'hidden',
|
||||
'text-overflow': 'ellipsis'
|
||||
},
|
||||
},
|
||||
{ // copy
|
||||
displayName: '',
|
||||
ariaLabel: loc.copy,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
isReadOnly: true,
|
||||
width: 50,
|
||||
headerCssStyles: {
|
||||
'border': 'none',
|
||||
},
|
||||
rowCssStyles: {
|
||||
'border-top': 'solid 1px #ccc',
|
||||
'border-bottom': 'solid 1px #ccc',
|
||||
'border-left': 'none',
|
||||
'border-right': 'none'
|
||||
}
|
||||
}
|
||||
],
|
||||
data: [],
|
||||
ariaLabel: loc.serviceEndpoints
|
||||
}).component();
|
||||
|
||||
this.endpointsDisplayContainer = this.modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
|
||||
this.endpointsDisplayContainer.addItem(this.endpointsTable);
|
||||
|
||||
// Note we don't make the table a child of the loading component since making the loading component align correctly
|
||||
// messes up the layout for the table that we display after loading is finished. Instead we'll just remove the loading
|
||||
// component once it's finished loading the content
|
||||
this.endpointsLoadingComponent = this.modelView.modelBuilder.loadingComponent()
|
||||
.withProps({ CSSStyles: { 'padding-top': '0px', 'padding-bottom': '0px' } })
|
||||
.component();
|
||||
this.endpointsDisplayContainer.addItem(this.endpointsLoadingComponent, { flex: '0 0 auto', CSSStyles: { 'padding-left': '150px', width: '30px' } });
|
||||
|
||||
endpointsContainer.addItem(this.endpointsErrorMessage);
|
||||
endpointsContainer.addItem(this.endpointsDisplayContainer);
|
||||
rootContainer.addItem(endpointsContainer, { flex: '0 0 auto' });
|
||||
|
||||
this.initialized = true;
|
||||
|
||||
// Now that we've created the UI load data from the model in case it already had data
|
||||
this.handleEndpointsUpdate(this.model.serviceEndpoints);
|
||||
this.handleBdcStatusUpdate(this.model.bdcStatus);
|
||||
|
||||
return rootContainer;
|
||||
}
|
||||
|
||||
public onRefreshStarted(): void {
|
||||
this.propertiesErrorMessage.display = 'none';
|
||||
this.serviceStatusErrorMessage.display = 'none';
|
||||
this.endpointsErrorMessage.display = 'none';
|
||||
|
||||
this.serviceStatusDisplayContainer.display = undefined;
|
||||
this.propertiesContainerLoadingComponent.display = undefined;
|
||||
this.endpointsDisplayContainer.display = undefined;
|
||||
}
|
||||
|
||||
private handleBdcStatusUpdate(bdcStatus?: BdcStatusModel): void {
|
||||
if (!bdcStatus) {
|
||||
return;
|
||||
}
|
||||
this.lastUpdatedLabel.value = loc.lastUpdated(this.model.bdcStatusLastUpdated);
|
||||
|
||||
this.propertiesContainerLoadingComponent.loading = false;
|
||||
|
||||
(<azdata.PropertiesContainerComponentProperties>this.propertiesContainerLoadingComponent.component).propertyItems = [
|
||||
{ displayName: loc.clusterState, value: getStateDisplayText(bdcStatus.state) },
|
||||
{ displayName: loc.healthStatus, value: getHealthStatusDisplayText(bdcStatus.healthStatus) }
|
||||
];
|
||||
|
||||
if (bdcStatus.services) {
|
||||
this.serviceStatusTable.data = bdcStatus.services.map(serviceStatus => {
|
||||
const statusIconCell = this.modelView.modelBuilder.text()
|
||||
.withProps({
|
||||
value: getHealthStatusIcon(serviceStatus.healthStatus),
|
||||
ariaRole: 'img',
|
||||
title: getHealthStatusDisplayText(serviceStatus.healthStatus),
|
||||
CSSStyles: { 'user-select': 'none', ...cssStyles.text }
|
||||
}).component();
|
||||
const nameCell = this.modelView.modelBuilder.hyperlink()
|
||||
.withProps({
|
||||
label: getServiceNameDisplayText(serviceStatus.serviceName),
|
||||
url: '',
|
||||
CSSStyles: { ...cssStyles.text }
|
||||
}).component();
|
||||
nameCell.onDidClick(() => {
|
||||
this.dashboard.selectTab(serviceStatus.serviceName);
|
||||
});
|
||||
|
||||
const viewDetailsButton = serviceStatus.healthStatus !== 'healthy' && serviceStatus.details && serviceStatus.details.length > 0 ? createViewDetailsButton(this.modelView.modelBuilder, serviceStatus.details) : undefined;
|
||||
return [
|
||||
statusIconCell,
|
||||
nameCell,
|
||||
getStateDisplayText(serviceStatus.state),
|
||||
getHealthStatusDisplayText(serviceStatus.healthStatus),
|
||||
viewDetailsButton];
|
||||
});
|
||||
this.serviceStatusDisplayContainer.removeItem(this.serviceStatusLoadingComponent);
|
||||
}
|
||||
}
|
||||
|
||||
private handleEndpointsUpdate(endpoints?: EndpointModel[]): void {
|
||||
if (!endpoints) {
|
||||
return;
|
||||
}
|
||||
// Sort the endpoints. The sort method is that SQL Server Master is first - followed by all
|
||||
// others in alphabetical order by endpoint
|
||||
const sqlServerMasterEndpoints = endpoints.filter(e => e.name === Endpoint.sqlServerMaster);
|
||||
endpoints = endpoints.filter(e => e.name !== Endpoint.sqlServerMaster)
|
||||
.sort((e1, e2) => {
|
||||
if (e1.endpoint < e2.endpoint) { return -1; }
|
||||
if (e1.endpoint > e2.endpoint) { return 1; }
|
||||
return 0;
|
||||
});
|
||||
endpoints.unshift(...sqlServerMasterEndpoints);
|
||||
|
||||
this.endpointsTable.dataValues = endpoints.map(e => {
|
||||
const copyValueCell = this.modelView.modelBuilder.button().withProps({ title: loc.copy }).component();
|
||||
copyValueCell.iconPath = IconPathHelper.copy;
|
||||
copyValueCell.onDidClick(() => {
|
||||
vscode.env.clipboard.writeText(e.endpoint);
|
||||
vscode.window.showInformationMessage(loc.copiedEndpoint(getEndpointDisplayText(e.name, e.description)));
|
||||
});
|
||||
return [{ value: getEndpointDisplayText(e.name, e.description) },
|
||||
{ value: createEndpointComponent(this.modelView.modelBuilder, e, this.model, hyperlinkedEndpoints.some(he => he === e.name)) },
|
||||
{ value: copyValueCell }];
|
||||
});
|
||||
|
||||
this.endpointsDisplayContainer.removeItem(this.endpointsLoadingComponent);
|
||||
}
|
||||
|
||||
private handleBdcError(errorEvent: BdcErrorEvent): void {
|
||||
if (errorEvent.errorType === 'bdcEndpoints') {
|
||||
const errorMessage = loc.endpointsError(errorEvent.error.message);
|
||||
this.showEndpointsError(errorMessage);
|
||||
} else if (errorEvent.errorType === 'bdcStatus') {
|
||||
this.showBdcStatusError(getBdcStatusErrorMessage(errorEvent.error));
|
||||
} else {
|
||||
this.handleGeneralError(errorEvent.error);
|
||||
}
|
||||
}
|
||||
|
||||
private showBdcStatusError(errorMessage: string): void {
|
||||
this.serviceStatusDisplayContainer.display = 'none';
|
||||
this.propertiesContainerLoadingComponent.display = 'none';
|
||||
this.serviceStatusErrorMessage.value = errorMessage;
|
||||
this.serviceStatusErrorMessage.display = undefined;
|
||||
this.propertiesErrorMessage.value = errorMessage;
|
||||
this.propertiesErrorMessage.display = undefined;
|
||||
}
|
||||
|
||||
private showEndpointsError(errorMessage: string): void {
|
||||
this.endpointsDisplayContainer.display = 'none';
|
||||
this.endpointsErrorMessage.display = undefined;
|
||||
this.endpointsErrorMessage.value = errorMessage;
|
||||
}
|
||||
|
||||
private handleGeneralError(error: Error): void {
|
||||
if (error instanceof HdfsDialogCancelledError) {
|
||||
const errorMessage = loc.noConnectionError;
|
||||
this.showBdcStatusError(errorMessage);
|
||||
this.showEndpointsError(errorMessage);
|
||||
} else {
|
||||
const errorMessage = loc.unexpectedError(error);
|
||||
this.showBdcStatusError(errorMessage);
|
||||
this.showEndpointsError(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createEndpointComponent(modelBuilder: azdata.ModelBuilder, endpoint: EndpointModel, bdcModel: BdcDashboardModel, isHyperlink: boolean): azdata.HyperlinkComponent | azdata.TextComponent {
|
||||
if (isHyperlink) {
|
||||
return modelBuilder.hyperlink()
|
||||
.withProps({
|
||||
label: endpoint.endpoint,
|
||||
title: endpoint.endpoint,
|
||||
url: endpoint.endpoint
|
||||
})
|
||||
.component();
|
||||
}
|
||||
else if (endpoint.name === Endpoint.sqlServerMaster) {
|
||||
const endpointCell = modelBuilder.hyperlink()
|
||||
.withProps({
|
||||
title: endpoint.endpoint,
|
||||
label: endpoint.endpoint,
|
||||
url: '',
|
||||
CSSStyles: { ...cssStyles.text }
|
||||
}).component();
|
||||
endpointCell.onDidClick(async () => {
|
||||
const connProfile = bdcModel.getSqlServerMasterConnectionProfile();
|
||||
const result = await azdata.connection.connect(connProfile, true, true);
|
||||
if (!result.connected) {
|
||||
if (result.errorMessage && result.errorMessage.length > 0) {
|
||||
vscode.window.showErrorMessage(result.errorMessage);
|
||||
}
|
||||
// Clear out the password and username before connecting since those being wrong are likely the issue
|
||||
connProfile.userName = undefined;
|
||||
connProfile.password = undefined;
|
||||
azdata.connection.openConnectionDialog(undefined, connProfile);
|
||||
}
|
||||
});
|
||||
return endpointCell;
|
||||
}
|
||||
else {
|
||||
return modelBuilder.text()
|
||||
.withProps({
|
||||
value: endpoint.endpoint,
|
||||
title: endpoint.endpoint,
|
||||
CSSStyles: { ...cssStyles.text }
|
||||
})
|
||||
.component();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IconPathHelper } from '../constants';
|
||||
import { BdcDashboardModel, getTroubleshootNotebookUrl } from './bdcDashboardModel';
|
||||
import * as loc from '../localizedConstants';
|
||||
import * as azdata from 'azdata';
|
||||
import * as vscode from 'vscode';
|
||||
import { InitializingComponent } from './intializingComponent';
|
||||
|
||||
export abstract class BdcDashboardPage extends InitializingComponent {
|
||||
|
||||
private _toolbarContainer: azdata.ToolbarContainer;
|
||||
private _refreshButton: azdata.ButtonComponent;
|
||||
|
||||
constructor(protected model: BdcDashboardModel, protected modelView: azdata.ModelView, protected serviceName?: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
public get toolbarContainer(): azdata.ToolbarContainer {
|
||||
// Lazily create the container only when needed
|
||||
if (!this._toolbarContainer) {
|
||||
this._toolbarContainer = this.createToolbarContainer();
|
||||
}
|
||||
return this._toolbarContainer;
|
||||
}
|
||||
|
||||
protected createToolbarContainer(): azdata.ToolbarContainer {
|
||||
// Refresh button
|
||||
this._refreshButton = this.modelView.modelBuilder.button()
|
||||
.withProps({
|
||||
label: loc.refresh,
|
||||
iconPath: IconPathHelper.refresh
|
||||
}).component();
|
||||
|
||||
this._refreshButton.onDidClick(async () => {
|
||||
await this.doRefresh();
|
||||
});
|
||||
|
||||
const openTroubleshootNotebookButton = this.modelView.modelBuilder.button()
|
||||
.withProps({
|
||||
label: loc.troubleshoot,
|
||||
iconPath: IconPathHelper.notebook
|
||||
}).component();
|
||||
|
||||
openTroubleshootNotebookButton.onDidClick(() => {
|
||||
vscode.commands.executeCommand('books.sqlserver2019', getTroubleshootNotebookUrl(this.serviceName));
|
||||
});
|
||||
|
||||
return this.modelView.modelBuilder.toolbarContainer()
|
||||
.withToolbarItems(
|
||||
[
|
||||
{ component: this._refreshButton },
|
||||
{ component: openTroubleshootNotebookButton }
|
||||
]
|
||||
).component();
|
||||
}
|
||||
|
||||
private async doRefresh(): Promise<void> {
|
||||
try {
|
||||
this._refreshButton.enabled = false;
|
||||
await this.model.refresh();
|
||||
} finally {
|
||||
this._refreshButton.enabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { BdcDashboardModel } from './bdcDashboardModel';
|
||||
import { BdcStatusModel, InstanceStatusModel, ResourceStatusModel } from '../controller/apiGenerated';
|
||||
import { getHealthStatusDisplayText, getHealthStatusIcon, getStateDisplayText, Service } from '../utils';
|
||||
import { cssStyles } from '../constants';
|
||||
import { isNullOrUndefined } from 'util';
|
||||
import { createViewDetailsButton } from './commonControls';
|
||||
import { BdcDashboardPage } from './bdcDashboardPage';
|
||||
import * as loc from '../localizedConstants';
|
||||
|
||||
export class BdcDashboardResourceStatusPage extends BdcDashboardPage {
|
||||
|
||||
private resourceStatusModel: ResourceStatusModel;
|
||||
private rootContainer: azdata.FlexContainer;
|
||||
private instanceHealthStatusTable: azdata.DeclarativeTableComponent;
|
||||
private metricsAndLogsRowsTable: azdata.DeclarativeTableComponent;
|
||||
private lastUpdatedLabel: azdata.TextComponent;
|
||||
|
||||
constructor(model: BdcDashboardModel, modelView: azdata.ModelView, serviceName: string, private resourceName: string) {
|
||||
super(model, modelView, serviceName);
|
||||
this.model.onDidUpdateBdcStatus(bdcStatus => this.eventuallyRunOnInitialized(() => this.handleBdcStatusUpdate(bdcStatus)));
|
||||
}
|
||||
|
||||
public get container(): azdata.FlexContainer {
|
||||
// Lazily create the container only when needed
|
||||
if (!this.rootContainer) {
|
||||
// We do this here so that we can have the resource model to use for populating the data
|
||||
// in the tables. This is to get around a timing issue with ModelView tables
|
||||
this.updateResourceStatusModel(this.model.bdcStatus);
|
||||
this.createContainer();
|
||||
}
|
||||
return this.rootContainer;
|
||||
}
|
||||
|
||||
private createContainer(): void {
|
||||
this.rootContainer = this.modelView.modelBuilder.flexContainer().withLayout(
|
||||
{
|
||||
flexFlow: 'column',
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
}).component();
|
||||
|
||||
// ##############################
|
||||
// # INSTANCE HEALTH AND STATUS #
|
||||
// ##############################
|
||||
|
||||
const healthStatusHeaderContainer = this.modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'row', height: '20px' }).component();
|
||||
this.rootContainer.addItem(healthStatusHeaderContainer, { CSSStyles: { 'padding-left': '10px', 'padding-top': '15px' } });
|
||||
|
||||
// Header label
|
||||
const healthStatusHeaderLabel = this.modelView.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.healthStatusDetails,
|
||||
CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '10px' }
|
||||
})
|
||||
.component();
|
||||
|
||||
healthStatusHeaderContainer.addItem(healthStatusHeaderLabel, { CSSStyles: { ...cssStyles.title } });
|
||||
|
||||
// Last updated label
|
||||
this.lastUpdatedLabel = this.modelView.modelBuilder.text()
|
||||
.withProps({
|
||||
value: loc.lastUpdated(this.model.bdcStatusLastUpdated),
|
||||
CSSStyles: { ...cssStyles.lastUpdatedText }
|
||||
}).component();
|
||||
|
||||
healthStatusHeaderContainer.addItem(this.lastUpdatedLabel, { CSSStyles: { 'margin-left': '45px' } });
|
||||
|
||||
this.instanceHealthStatusTable = this.modelView.modelBuilder.declarativeTable()
|
||||
.withProps(
|
||||
{
|
||||
columns: [
|
||||
{ // status icon
|
||||
displayName: '',
|
||||
ariaLabel: loc.statusIcon,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
isReadOnly: true,
|
||||
width: 25,
|
||||
headerCssStyles: {
|
||||
'border': 'none'
|
||||
},
|
||||
rowCssStyles: {
|
||||
'border-top': 'solid 1px #ccc',
|
||||
'border-bottom': 'solid 1px #ccc',
|
||||
'border-left': 'none',
|
||||
'border-right': 'none'
|
||||
},
|
||||
},
|
||||
{ // instance
|
||||
displayName: loc.instance,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: 100,
|
||||
headerCssStyles: {
|
||||
'border': 'none',
|
||||
...cssStyles.tableHeader
|
||||
},
|
||||
rowCssStyles: {
|
||||
'border-top': 'solid 1px #ccc',
|
||||
'border-bottom': 'solid 1px #ccc',
|
||||
'border-left': 'none',
|
||||
'border-right': 'none'
|
||||
},
|
||||
},
|
||||
{ // state
|
||||
displayName: loc.state,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: 150,
|
||||
headerCssStyles: {
|
||||
'border': 'none',
|
||||
...cssStyles.tableHeader
|
||||
},
|
||||
rowCssStyles: {
|
||||
'border-top': 'solid 1px #ccc',
|
||||
'border-bottom': 'solid 1px #ccc',
|
||||
'border-left': 'none',
|
||||
'border-right': 'none'
|
||||
},
|
||||
},
|
||||
{ // health status
|
||||
displayName: loc.healthStatus,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: 100,
|
||||
headerCssStyles: {
|
||||
'border': 'none',
|
||||
'text-align': 'left',
|
||||
...cssStyles.tableHeader
|
||||
},
|
||||
rowCssStyles: {
|
||||
'border-top': 'solid 1px #ccc',
|
||||
'border-bottom': 'solid 1px #ccc',
|
||||
'border-left': 'none',
|
||||
'border-right': 'none'
|
||||
}
|
||||
},
|
||||
{ // view details button
|
||||
displayName: '',
|
||||
ariaLabel: loc.viewErrorDetails,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
isReadOnly: true,
|
||||
width: 150,
|
||||
headerCssStyles: {
|
||||
'border': 'none'
|
||||
},
|
||||
rowCssStyles: {
|
||||
'border-top': 'solid 1px #ccc',
|
||||
'border-bottom': 'solid 1px #ccc',
|
||||
'border-left': 'none',
|
||||
'border-right': 'none'
|
||||
},
|
||||
},
|
||||
],
|
||||
data: this.createHealthStatusRows(),
|
||||
ariaLabel: loc.healthStatusDetails
|
||||
}).component();
|
||||
this.rootContainer.addItem(this.instanceHealthStatusTable, { flex: '0 0 auto' });
|
||||
|
||||
// ####################
|
||||
// # METRICS AND LOGS #
|
||||
// ####################
|
||||
|
||||
// Title label
|
||||
const endpointsLabel = this.modelView.modelBuilder.text()
|
||||
.withProps({ value: loc.metricsAndLogs, CSSStyles: { 'margin-block-start': '20px', 'margin-block-end': '0px' } })
|
||||
.component();
|
||||
this.rootContainer.addItem(endpointsLabel, { CSSStyles: { 'padding-left': '10px', ...cssStyles.title } });
|
||||
|
||||
let metricsAndLogsColumns: azdata.DeclarativeTableColumn[] =
|
||||
[
|
||||
{ // instance
|
||||
displayName: loc.instance,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
isReadOnly: true,
|
||||
width: 125,
|
||||
headerCssStyles: {
|
||||
'border': 'none',
|
||||
...cssStyles.tableHeader
|
||||
},
|
||||
rowCssStyles: {
|
||||
'border-top': 'solid 1px #ccc',
|
||||
'border-bottom': 'solid 1px #ccc',
|
||||
'border-left': 'none',
|
||||
'border-right': 'none'
|
||||
}
|
||||
},
|
||||
{ // node metrics
|
||||
displayName: loc.nodeMetrics,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
isReadOnly: true,
|
||||
width: 100,
|
||||
headerCssStyles: {
|
||||
'border': 'none',
|
||||
...cssStyles.tableHeader
|
||||
},
|
||||
rowCssStyles: {
|
||||
'border-top': 'solid 1px #ccc',
|
||||
'border-bottom': 'solid 1px #ccc',
|
||||
'border-left': 'none',
|
||||
'border-right': 'none'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Only show SQL metrics column for SQL resource instances
|
||||
if (this.serviceName.toLowerCase() === Service.sql) {
|
||||
metricsAndLogsColumns.push(
|
||||
{ // sql metrics
|
||||
displayName: loc.sqlMetrics,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
isReadOnly: true,
|
||||
width: 100,
|
||||
headerCssStyles: {
|
||||
'border': 'none',
|
||||
'text-align': 'left',
|
||||
...cssStyles.tableHeader
|
||||
},
|
||||
rowCssStyles: {
|
||||
'border-top': 'solid 1px #ccc',
|
||||
'border-bottom': 'solid 1px #ccc',
|
||||
'border-left': 'none',
|
||||
'border-right': 'none'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
metricsAndLogsColumns.push(
|
||||
{ // logs
|
||||
displayName: loc.logs,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
isReadOnly: true,
|
||||
width: 100,
|
||||
headerCssStyles: {
|
||||
'border': 'none',
|
||||
'text-align': 'left',
|
||||
...cssStyles.tableHeader
|
||||
},
|
||||
rowCssStyles: {
|
||||
'border-top': 'solid 1px #ccc',
|
||||
'border-bottom': 'solid 1px #ccc',
|
||||
'border-left': 'none',
|
||||
'border-right': 'none'
|
||||
}
|
||||
});
|
||||
|
||||
this.metricsAndLogsRowsTable = this.modelView.modelBuilder.declarativeTable()
|
||||
.withProps(
|
||||
{
|
||||
columns: metricsAndLogsColumns,
|
||||
data: this.createMetricsAndLogsRows(),
|
||||
ariaLabel: loc.metricsAndLogs
|
||||
}).component();
|
||||
this.rootContainer.addItem(this.metricsAndLogsRowsTable, { flex: '0 0 auto' });
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
private updateResourceStatusModel(bdcStatus?: BdcStatusModel): void {
|
||||
// If we can't find the resource model for this resource then just
|
||||
// default to keeping what we had originally
|
||||
if (!bdcStatus) {
|
||||
return;
|
||||
}
|
||||
const service = bdcStatus.services ? bdcStatus.services.find(s => s.serviceName === this.serviceName) : undefined;
|
||||
this.resourceStatusModel = service ? service.resources.find(r => r.resourceName === this.resourceName) : this.resourceStatusModel;
|
||||
}
|
||||
|
||||
private handleBdcStatusUpdate(bdcStatus?: BdcStatusModel): void {
|
||||
this.updateResourceStatusModel(bdcStatus);
|
||||
|
||||
if (!this.resourceStatusModel || isNullOrUndefined(this.resourceStatusModel.instances)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastUpdatedLabel.value = loc.lastUpdated(this.model.bdcStatusLastUpdated);
|
||||
|
||||
this.instanceHealthStatusTable.data = this.createHealthStatusRows();
|
||||
|
||||
this.metricsAndLogsRowsTable.data = this.createMetricsAndLogsRows();
|
||||
}
|
||||
|
||||
private createMetricsAndLogsRows(): any[][] {
|
||||
return this.resourceStatusModel ? this.resourceStatusModel.instances.map(instanceStatus => this.createMetricsAndLogsRow(instanceStatus)) : [];
|
||||
}
|
||||
|
||||
private createHealthStatusRows(): any[][] {
|
||||
return this.resourceStatusModel ? this.resourceStatusModel.instances.map(instanceStatus => this.createHealthStatusRow(instanceStatus)) : [];
|
||||
}
|
||||
|
||||
private createMetricsAndLogsRow(instanceStatus: InstanceStatusModel): any[] {
|
||||
const row: any[] = [instanceStatus.instanceName];
|
||||
|
||||
// Not all instances have all logs available - in that case just display N/A instead of a link
|
||||
if (isNullOrUndefined(instanceStatus.dashboards) || isNullOrUndefined(instanceStatus.dashboards.nodeMetricsUrl)) {
|
||||
row.push(this.modelView.modelBuilder.text().withProps({ value: loc.notAvailable, CSSStyles: { ...cssStyles.text } }).component());
|
||||
} else {
|
||||
row.push(this.modelView.modelBuilder.hyperlink().withProps({
|
||||
label: loc.view,
|
||||
url: instanceStatus.dashboards.nodeMetricsUrl,
|
||||
title: instanceStatus.dashboards.nodeMetricsUrl,
|
||||
ariaLabel: loc.viewNodeMetrics(instanceStatus.dashboards.nodeMetricsUrl),
|
||||
CSSStyles: { ...cssStyles.text }
|
||||
}).component());
|
||||
}
|
||||
|
||||
// Only show SQL metrics column for SQL resource instances
|
||||
if (this.serviceName === Service.sql) {
|
||||
// Not all instances have all logs available - in that case just display N/A instead of a link
|
||||
if (isNullOrUndefined(instanceStatus.dashboards) || isNullOrUndefined(instanceStatus.dashboards.sqlMetricsUrl)) {
|
||||
row.push(this.modelView.modelBuilder.text().withProps({ value: loc.notAvailable, CSSStyles: { ...cssStyles.text } }).component());
|
||||
} else {
|
||||
row.push(this.modelView.modelBuilder.hyperlink().withProps({
|
||||
label: loc.view,
|
||||
url: instanceStatus.dashboards.sqlMetricsUrl,
|
||||
title: instanceStatus.dashboards.sqlMetricsUrl,
|
||||
ariaLabel: loc.viewSqlMetrics(instanceStatus.dashboards.sqlMetricsUrl),
|
||||
CSSStyles: { ...cssStyles.text }
|
||||
}).component());
|
||||
}
|
||||
}
|
||||
|
||||
if (isNullOrUndefined(instanceStatus.dashboards) || isNullOrUndefined(instanceStatus.dashboards.logsUrl)) {
|
||||
row.push(this.modelView.modelBuilder.text().withProps({ value: loc.notAvailable, CSSStyles: { ...cssStyles.text } }).component());
|
||||
} else {
|
||||
row.push(this.modelView.modelBuilder.hyperlink().withProps({
|
||||
label: loc.view,
|
||||
url: instanceStatus.dashboards.logsUrl,
|
||||
title: instanceStatus.dashboards.logsUrl,
|
||||
ariaLabel: loc.viewLogs(instanceStatus.dashboards.logsUrl),
|
||||
CSSStyles: { ...cssStyles.text }
|
||||
}).component());
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
private createHealthStatusRow(instanceStatus: InstanceStatusModel): any[] {
|
||||
const statusIconCell = this.modelView.modelBuilder.text()
|
||||
.withProps({
|
||||
value: getHealthStatusIcon(instanceStatus.healthStatus),
|
||||
ariaRole: 'img',
|
||||
title: getHealthStatusDisplayText(instanceStatus.healthStatus),
|
||||
CSSStyles: { 'user-select': 'none', ...cssStyles.text }
|
||||
}).component();
|
||||
|
||||
const viewDetailsButton = instanceStatus.healthStatus !== 'healthy' && instanceStatus.details && instanceStatus.details.length > 0 ? createViewDetailsButton(this.modelView.modelBuilder, instanceStatus.details) : undefined;
|
||||
return [
|
||||
statusIconCell,
|
||||
instanceStatus.instanceName,
|
||||
getStateDisplayText(instanceStatus.state),
|
||||
getHealthStatusDisplayText(instanceStatus.healthStatus),
|
||||
viewDetailsButton];
|
||||
}
|
||||
}
|
||||
@@ -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 { BdcStatusModel, ResourceStatusModel } from '../controller/apiGenerated';
|
||||
import { BdcDashboardResourceStatusPage } from './bdcDashboardResourceStatusPage';
|
||||
import { BdcDashboardModel } from './bdcDashboardModel';
|
||||
import { BdcDashboardPage } from './bdcDashboardPage';
|
||||
import { getHealthStatusDotIcon } from '../utils';
|
||||
|
||||
export class BdcServiceStatusPage extends BdcDashboardPage {
|
||||
|
||||
private createdResourceTabs: Map<string, azdata.Tab> = new Map<string, azdata.Tab>();
|
||||
private tabbedPanel: azdata.TabbedPanelComponent;
|
||||
|
||||
constructor(serviceName: string, model: BdcDashboardModel, modelView: azdata.ModelView) {
|
||||
super(model, modelView, serviceName);
|
||||
this.model.onDidUpdateBdcStatus(bdcStatus => this.eventuallyRunOnInitialized(() => this.handleBdcStatusUpdate(bdcStatus)));
|
||||
}
|
||||
|
||||
public get container(): azdata.TabbedPanelComponent {
|
||||
// Lazily create the container only when needed
|
||||
if (!this.tabbedPanel) {
|
||||
this.createPage();
|
||||
}
|
||||
return this.tabbedPanel;
|
||||
}
|
||||
|
||||
private createPage(): void {
|
||||
this.tabbedPanel = this.modelView.modelBuilder.tabbedPanel()
|
||||
.withLayout({ showIcon: true, alwaysShowTabs: true }).component();
|
||||
|
||||
// Initialize our set of tab pages
|
||||
this.handleBdcStatusUpdate(this.model.bdcStatus);
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
private handleBdcStatusUpdate(bdcStatus: BdcStatusModel): void {
|
||||
if (!bdcStatus) {
|
||||
return;
|
||||
}
|
||||
const service = bdcStatus.services.find(s => s.serviceName === this.serviceName);
|
||||
if (service && service.resources) {
|
||||
this.updateResourcePages(service.resources);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the resource tab pages, creating any new ones as necessary
|
||||
*/
|
||||
private updateResourcePages(resources: ResourceStatusModel[]): void {
|
||||
resources.forEach(resource => {
|
||||
const existingTab = this.createdResourceTabs.get(resource.resourceName);
|
||||
if (existingTab) {
|
||||
existingTab.icon = getHealthStatusDotIcon(resource.healthStatus);
|
||||
} else {
|
||||
const resourceStatusPage = new BdcDashboardResourceStatusPage(this.model, this.modelView, this.serviceName, resource.resourceName);
|
||||
const newTab: azdata.Tab = {
|
||||
title: resource.resourceName,
|
||||
id: resource.resourceName,
|
||||
content: resourceStatusPage.container,
|
||||
icon: getHealthStatusDotIcon(resource.healthStatus)
|
||||
};
|
||||
this.createdResourceTabs.set(resource.resourceName, newTab);
|
||||
}
|
||||
});
|
||||
this.tabbedPanel.updateTabs(Array.from(this.createdResourceTabs.values()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 * as loc from '../localizedConstants';
|
||||
|
||||
export function createViewDetailsButton(modelBuilder: azdata.ModelBuilder, text: string): azdata.ButtonComponent {
|
||||
const viewDetailsButton = modelBuilder.button().withProps({
|
||||
label: loc.viewDetails,
|
||||
ariaLabel: loc.viewErrorDetails,
|
||||
secondary: true
|
||||
}).component();
|
||||
viewDetailsButton.onDidClick(() => {
|
||||
vscode.window.showErrorMessage(text, { modal: true });
|
||||
});
|
||||
return viewDetailsButton;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { HdfsDialogBase, HdfsDialogModelBase, HdfsDialogProperties } from './hdfsDialogBase';
|
||||
import { ClusterController } from '../controller/clusterControllerApi';
|
||||
import * as loc from '../localizedConstants';
|
||||
|
||||
|
||||
export class ConnectControllerDialog extends HdfsDialogBase<HdfsDialogProperties, ClusterController> {
|
||||
constructor(model: ConnectControllerModel) {
|
||||
super(loc.connectToController, model);
|
||||
}
|
||||
|
||||
protected getMainSectionComponents(): (azdata.FormComponentGroup | azdata.FormComponent)[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
protected async validate(): Promise<{ validated: boolean, value?: ClusterController }> {
|
||||
try {
|
||||
const controller = await this.model.onComplete({
|
||||
url: this.urlInputBox && this.urlInputBox.value,
|
||||
auth: this.authValue,
|
||||
username: this.usernameInputBox && this.usernameInputBox.value,
|
||||
password: this.passwordInputBox && this.passwordInputBox.value
|
||||
});
|
||||
return { validated: true, value: controller };
|
||||
} catch (error) {
|
||||
await this.reportError(error);
|
||||
return { validated: false, value: undefined };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ConnectControllerModel extends HdfsDialogModelBase<HdfsDialogProperties, ClusterController> {
|
||||
|
||||
constructor(props: HdfsDialogProperties) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
protected async handleCompleted(): Promise<ClusterController> {
|
||||
this.throwIfMissingUsernamePassword();
|
||||
|
||||
// We pre-fetch the endpoints here to verify that the information entered is correct (the user is able to connect)
|
||||
return await this.createAndVerifyControllerConnection();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { ClusterController, ControllerError } from '../controller/clusterControllerApi';
|
||||
import { Deferred } from '../../common/promise';
|
||||
import * as loc from '../localizedConstants';
|
||||
import { AuthType, IEndPointsResponse } from 'bdc';
|
||||
|
||||
function getAuthCategory(name: AuthType): azdata.CategoryValue {
|
||||
if (name === 'basic') {
|
||||
return { name: name, displayName: loc.basic };
|
||||
}
|
||||
return { name: name, displayName: loc.windowsAuth };
|
||||
}
|
||||
|
||||
export interface HdfsDialogProperties {
|
||||
url?: string;
|
||||
auth?: AuthType;
|
||||
username?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export class HdfsDialogCancelledError extends Error {
|
||||
constructor(message: string = 'Dialog cancelled') {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class HdfsDialogModelBase<T extends HdfsDialogProperties, R> {
|
||||
protected _canceled = false;
|
||||
private _authTypes: azdata.CategoryValue[];
|
||||
constructor(
|
||||
public props: T
|
||||
) {
|
||||
if (!props.auth) {
|
||||
this.props.auth = 'basic';
|
||||
}
|
||||
}
|
||||
|
||||
public get authCategories(): azdata.CategoryValue[] {
|
||||
if (!this._authTypes) {
|
||||
this._authTypes = [getAuthCategory('basic'), getAuthCategory('integrated')];
|
||||
}
|
||||
return this._authTypes;
|
||||
}
|
||||
|
||||
public get authCategory(): azdata.CategoryValue {
|
||||
return getAuthCategory(this.props.auth);
|
||||
}
|
||||
|
||||
public async onComplete(props: T): Promise<R | undefined> {
|
||||
try {
|
||||
this.props = props;
|
||||
return await this.handleCompleted();
|
||||
} catch (error) {
|
||||
// Ignore the error if we cancelled the request since we can't stop the actual request from completing
|
||||
if (!this._canceled) {
|
||||
throw error;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract handleCompleted(): Promise<R>;
|
||||
|
||||
public async onError(error: ControllerError): Promise<void> {
|
||||
// implement
|
||||
}
|
||||
|
||||
public async onCancel(): Promise<void> {
|
||||
this._canceled = true;
|
||||
}
|
||||
|
||||
protected createController(): ClusterController {
|
||||
return new ClusterController(this.props.url, this.props.auth, this.props.username, this.props.password);
|
||||
}
|
||||
|
||||
protected async createAndVerifyControllerConnection(): Promise<ClusterController> {
|
||||
// We pre-fetch the endpoints here to verify that the information entered is correct (the user is able to connect)
|
||||
let controller = this.createController();
|
||||
let response: IEndPointsResponse;
|
||||
try {
|
||||
response = await controller.getEndPoints();
|
||||
if (!response || !response.endPoints) {
|
||||
throw new Error(loc.loginFailed);
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(loc.loginFailedWithError(err));
|
||||
}
|
||||
return controller;
|
||||
}
|
||||
|
||||
protected throwIfMissingUsernamePassword(): void {
|
||||
if (this.props.auth === 'basic') {
|
||||
// Verify username and password as we can't make them required in the UI
|
||||
if (!this.props.username) {
|
||||
throw new Error(loc.usernameRequired);
|
||||
} else if (!this.props.password) {
|
||||
throw new Error(loc.passwordRequired);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class HdfsDialogBase<T extends HdfsDialogProperties, R> {
|
||||
|
||||
protected dialog: azdata.window.Dialog;
|
||||
protected uiModelBuilder!: azdata.ModelBuilder;
|
||||
|
||||
protected urlInputBox!: azdata.InputBoxComponent;
|
||||
protected authDropdown!: azdata.DropDownComponent;
|
||||
protected usernameInputBox!: azdata.InputBoxComponent;
|
||||
protected passwordInputBox!: azdata.InputBoxComponent;
|
||||
|
||||
private returnPromise: Deferred<R>;
|
||||
|
||||
constructor(private title: string, protected model: HdfsDialogModelBase<T, R>) {
|
||||
}
|
||||
|
||||
public async showDialog(): Promise<R> {
|
||||
this.returnPromise = new Deferred<R>();
|
||||
this.createDialog();
|
||||
azdata.window.openDialog(this.dialog);
|
||||
return this.returnPromise.promise;
|
||||
}
|
||||
|
||||
private createDialog(): void {
|
||||
this.dialog = azdata.window.createModelViewDialog(this.title);
|
||||
this.dialog.registerContent(async view => {
|
||||
this.uiModelBuilder = view.modelBuilder;
|
||||
|
||||
this.urlInputBox = this.uiModelBuilder.inputBox()
|
||||
.withProps({
|
||||
placeHolder: loc.url.toLocaleLowerCase(),
|
||||
value: this.model.props.url,
|
||||
enabled: false
|
||||
}).component();
|
||||
|
||||
this.authDropdown = this.uiModelBuilder.dropDown().withProps({
|
||||
values: this.model.authCategories,
|
||||
value: this.model.authCategory,
|
||||
editable: false,
|
||||
}).component();
|
||||
this.authDropdown.onValueChanged(e => this.onAuthChanged());
|
||||
this.usernameInputBox = this.uiModelBuilder.inputBox()
|
||||
.withProps({
|
||||
placeHolder: loc.username.toLocaleLowerCase(),
|
||||
value: this.model.props.username
|
||||
}).component();
|
||||
this.passwordInputBox = this.uiModelBuilder.inputBox()
|
||||
.withProps({
|
||||
placeHolder: loc.password.toLocaleLowerCase(),
|
||||
inputType: 'password',
|
||||
value: this.model.props.password
|
||||
})
|
||||
.component();
|
||||
|
||||
let connectionSection: azdata.FormComponentGroup = {
|
||||
components: [
|
||||
{
|
||||
component: this.urlInputBox,
|
||||
title: loc.clusterUrl,
|
||||
required: true
|
||||
}, {
|
||||
component: this.authDropdown,
|
||||
title: loc.authType,
|
||||
required: true
|
||||
}, {
|
||||
component: this.usernameInputBox,
|
||||
title: loc.username,
|
||||
required: false
|
||||
}, {
|
||||
component: this.passwordInputBox,
|
||||
title: loc.password,
|
||||
required: false
|
||||
}
|
||||
],
|
||||
title: loc.clusterConnection
|
||||
};
|
||||
let formModel = this.uiModelBuilder.formContainer()
|
||||
.withFormItems(
|
||||
this.getMainSectionComponents().concat(
|
||||
connectionSection)
|
||||
).withLayout({ width: '100%' }).component();
|
||||
|
||||
await view.initializeModel(formModel);
|
||||
this.onAuthChanged();
|
||||
});
|
||||
|
||||
this.dialog.registerCloseValidator(async () => {
|
||||
const result = await this.validate();
|
||||
if (result.validated) {
|
||||
this.returnPromise.resolve(result.value);
|
||||
this.returnPromise = undefined;
|
||||
}
|
||||
return result.validated;
|
||||
});
|
||||
this.dialog.cancelButton.onClick(async () => await this.cancel());
|
||||
this.dialog.okButton.label = loc.ok;
|
||||
this.dialog.cancelButton.label = loc.cancel;
|
||||
}
|
||||
|
||||
protected abstract getMainSectionComponents(): (azdata.FormComponentGroup | azdata.FormComponent)[];
|
||||
|
||||
protected get authValue(): AuthType {
|
||||
return (<azdata.CategoryValue>this.authDropdown.value).name as AuthType;
|
||||
}
|
||||
|
||||
private onAuthChanged(): void {
|
||||
let isBasic = this.authValue === 'basic';
|
||||
this.usernameInputBox.enabled = isBasic;
|
||||
this.passwordInputBox.enabled = isBasic;
|
||||
if (!isBasic) {
|
||||
this.usernameInputBox.value = '';
|
||||
this.passwordInputBox.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract validate(): Promise<{ validated: boolean, value?: R }>;
|
||||
|
||||
private async cancel(): Promise<void> {
|
||||
if (this.model && this.model.onCancel) {
|
||||
await this.model.onCancel();
|
||||
}
|
||||
this.returnPromise.reject(new HdfsDialogCancelledError());
|
||||
}
|
||||
|
||||
protected async reportError(error: any): Promise<void> {
|
||||
this.dialog.message = {
|
||||
text: (typeof error === 'string') ? error : error.message,
|
||||
level: azdata.window.MessageLevel.Error
|
||||
};
|
||||
if (this.model && this.model.onError) {
|
||||
await this.model.onError(error as ControllerError);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Deferred } from '../../common/promise';
|
||||
|
||||
export abstract class InitializingComponent {
|
||||
|
||||
private _initialized: boolean = false;
|
||||
|
||||
private _onInitializedPromise: Deferred<void> = new Deferred();
|
||||
|
||||
constructor() { }
|
||||
|
||||
protected get initialized(): boolean {
|
||||
return this._initialized;
|
||||
}
|
||||
|
||||
protected set initialized(value: boolean) {
|
||||
if (!this._initialized && value) {
|
||||
this._initialized = true;
|
||||
this._onInitializedPromise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the specified action when the component is initialized. If already initialized just runs
|
||||
* the action immediately.
|
||||
* @param action The action to be ran when the page is initialized
|
||||
*/
|
||||
protected eventuallyRunOnInitialized(action: () => void): void {
|
||||
if (!this._initialized) {
|
||||
this._onInitializedPromise.promise.then(() => action()).catch(error => console.error(`Unexpected error running onInitialized action for BDC Page : ${error}`));
|
||||
} else {
|
||||
action();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { ClusterController, MountInfo, MountState } from '../controller/clusterControllerApi';
|
||||
import { HdfsDialogBase, HdfsDialogModelBase, HdfsDialogProperties } from './hdfsDialogBase';
|
||||
import * as loc from '../localizedConstants';
|
||||
|
||||
/**
|
||||
* Converts a comma-delimited set of key value pair credentials to a JSON object.
|
||||
* This code is taken from the azdata implementation written in Python
|
||||
*/
|
||||
function convertCredsToJson(creds: string): { credentials: {} } {
|
||||
if (!creds) {
|
||||
return undefined;
|
||||
}
|
||||
let credObj: { 'credentials': { [key: string]: any } } = { 'credentials': {} };
|
||||
let pairs = creds.split(',');
|
||||
let validPairs: string[] = [];
|
||||
for (let i = 0; i < pairs.length; i++) {
|
||||
// handle escaped commas in a browser-agnostic way using regex:
|
||||
// this matches a string ending in a single escape character \, but not \\.
|
||||
// In this case we split on ',` when we should've ignored it as it was a \, instead.
|
||||
// Restore the escaped comma by combining the 2 split strings
|
||||
if (i < (pairs.length - 1) && pairs[i].match(/(?!\\).*\\$/)) {
|
||||
pairs[i + 1] = `${pairs[i]},${pairs[i + 1]}`;
|
||||
} else {
|
||||
validPairs.push(pairs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
validPairs.forEach(pair => {
|
||||
const formattingErr = loc.badCredentialsFormatting(pair);
|
||||
try {
|
||||
// # remove escaped characters for ,
|
||||
pair = pair.replace('\\,', ',').trim();
|
||||
let firstEquals = pair.indexOf('=');
|
||||
if (firstEquals <= 0 || firstEquals >= pair.length) {
|
||||
throw new Error(formattingErr);
|
||||
}
|
||||
let key = pair.substring(0, firstEquals);
|
||||
let value = pair.substring(firstEquals + 1);
|
||||
credObj.credentials[key] = value;
|
||||
} catch (err) {
|
||||
throw new Error(formattingErr);
|
||||
}
|
||||
});
|
||||
return credObj;
|
||||
}
|
||||
|
||||
export interface MountHdfsProperties extends HdfsDialogProperties {
|
||||
hdfsPath?: string;
|
||||
remoteUri?: string;
|
||||
credentials?: string;
|
||||
}
|
||||
|
||||
export class MountHdfsDialogModel extends HdfsDialogModelBase<MountHdfsProperties, void> {
|
||||
private credentials: {};
|
||||
|
||||
constructor(props: MountHdfsProperties) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
protected async handleCompleted(): Promise<void> {
|
||||
this.throwIfMissingUsernamePassword();
|
||||
// Validate credentials
|
||||
this.credentials = convertCredsToJson(this.props.credentials);
|
||||
|
||||
// We pre-fetch the endpoints here to verify that the information entered is correct (the user is able to connect)
|
||||
let controller = await this.createAndVerifyControllerConnection();
|
||||
if (this._canceled) {
|
||||
return;
|
||||
}
|
||||
azdata.tasks.startBackgroundOperation(
|
||||
{
|
||||
connection: undefined,
|
||||
displayName: loc.mountTask(this.props.hdfsPath),
|
||||
description: '',
|
||||
isCancelable: false,
|
||||
operation: op => {
|
||||
this.onSubmit(controller, op);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async onSubmit(controller: ClusterController, op: azdata.BackgroundOperation): Promise<void> {
|
||||
try {
|
||||
await controller.mountHdfs(this.props.hdfsPath, this.props.remoteUri, this.credentials);
|
||||
op.updateStatus(azdata.TaskStatus.InProgress, loc.mountTaskSubmitted);
|
||||
|
||||
// Wait until status has changed or some sensible time expired. If it goes over 2 minutes we assume it's "working"
|
||||
// as there's no other API that'll give us this for now
|
||||
let result = await this.waitOnMountStatusChange(controller);
|
||||
let msg = result.state === MountState.Ready ? loc.mountCompleted : loc.mountInProgress;
|
||||
op.updateStatus(azdata.TaskStatus.Succeeded, msg);
|
||||
} catch (error) {
|
||||
const errMsg = loc.mountError(error);
|
||||
vscode.window.showErrorMessage(errMsg);
|
||||
op.updateStatus(azdata.TaskStatus.Failed, errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
private waitOnMountStatusChange(controller: ClusterController): Promise<MountInfo> {
|
||||
return new Promise<MountInfo>((resolve, reject) => {
|
||||
const waitTime = 5 * 1000; // 5 seconds
|
||||
const maxRetries = 30; // 5 x 30 = 150 seconds. After this time, can assume things are "working" as 2 min timeout passed
|
||||
let waitOnChange = async (retries: number) => {
|
||||
try {
|
||||
let mountInfo = await this.getMountStatus(controller, this.props.hdfsPath);
|
||||
if (mountInfo && mountInfo.error || mountInfo.state === MountState.Error) {
|
||||
reject(new Error(mountInfo.error ? mountInfo.error : loc.mountErrorUnknown));
|
||||
} else if (mountInfo.state === MountState.Ready || retries <= 0) {
|
||||
resolve(mountInfo);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
waitOnChange(retries - 1).catch(e => reject(e));
|
||||
}, waitTime);
|
||||
}
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
};
|
||||
waitOnChange(maxRetries);
|
||||
});
|
||||
}
|
||||
|
||||
private async getMountStatus(controller: ClusterController, path: string): Promise<MountInfo> {
|
||||
let statusResponse = await controller.getMountStatus(path);
|
||||
if (statusResponse.mount) {
|
||||
return Array.isArray(statusResponse.mount) ? statusResponse.mount[0] : statusResponse.mount;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class MountHdfsDialog extends HdfsDialogBase<MountHdfsProperties, void> {
|
||||
private pathInputBox: azdata.InputBoxComponent;
|
||||
private remoteUriInputBox: azdata.InputBoxComponent;
|
||||
private credentialsInputBox: azdata.InputBoxComponent;
|
||||
|
||||
constructor(model: MountHdfsDialogModel) {
|
||||
super(loc.mountFolder, model);
|
||||
}
|
||||
|
||||
protected getMainSectionComponents(): (azdata.FormComponentGroup | azdata.FormComponent)[] {
|
||||
const newMountName = '/mymount';
|
||||
let pathVal = this.model.props.hdfsPath;
|
||||
pathVal = (!pathVal || pathVal === '/') ? newMountName : (pathVal + newMountName);
|
||||
this.pathInputBox = this.uiModelBuilder.inputBox()
|
||||
.withProps({
|
||||
value: pathVal
|
||||
}).component();
|
||||
this.remoteUriInputBox = this.uiModelBuilder.inputBox()
|
||||
.withProps({
|
||||
value: this.model.props.remoteUri
|
||||
})
|
||||
.component();
|
||||
this.credentialsInputBox = this.uiModelBuilder.inputBox()
|
||||
.withProps({
|
||||
inputType: 'password',
|
||||
value: this.model.props.credentials
|
||||
})
|
||||
.component();
|
||||
|
||||
return [
|
||||
{
|
||||
components: [
|
||||
{
|
||||
component: this.pathInputBox,
|
||||
title: loc.hdfsPath,
|
||||
required: true,
|
||||
layout: {
|
||||
info: loc.hdfsPathInfo
|
||||
}
|
||||
}, {
|
||||
component: this.remoteUriInputBox,
|
||||
title: loc.remoteUri,
|
||||
required: true,
|
||||
layout: {
|
||||
info: loc.remoteUriInfo
|
||||
}
|
||||
}, {
|
||||
component: this.credentialsInputBox,
|
||||
title: loc.credentials,
|
||||
required: false,
|
||||
layout: {
|
||||
info: loc.credentialsInfo
|
||||
}
|
||||
}
|
||||
],
|
||||
title: loc.mountConfiguration
|
||||
}];
|
||||
}
|
||||
|
||||
protected async validate(): Promise<{ validated: boolean }> {
|
||||
try {
|
||||
await this.model.onComplete({
|
||||
url: this.urlInputBox && this.urlInputBox.value,
|
||||
auth: this.authValue,
|
||||
username: this.usernameInputBox && this.usernameInputBox.value,
|
||||
password: this.passwordInputBox && this.passwordInputBox.value,
|
||||
hdfsPath: this.pathInputBox && this.pathInputBox.value,
|
||||
remoteUri: this.remoteUriInputBox && this.remoteUriInputBox.value,
|
||||
credentials: this.credentialsInputBox && this.credentialsInputBox.value
|
||||
});
|
||||
return { validated: true };
|
||||
} catch (error) {
|
||||
await this.reportError(error);
|
||||
return { validated: false };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class RefreshMountDialog extends HdfsDialogBase<MountHdfsProperties, void> {
|
||||
private pathInputBox: azdata.InputBoxComponent;
|
||||
|
||||
constructor(model: RefreshMountModel) {
|
||||
super(loc.refreshMount, model);
|
||||
}
|
||||
|
||||
protected getMainSectionComponents(): (azdata.FormComponentGroup | azdata.FormComponent)[] {
|
||||
this.pathInputBox = this.uiModelBuilder.inputBox()
|
||||
.withProps({
|
||||
value: this.model.props.hdfsPath
|
||||
}).component();
|
||||
return [
|
||||
{
|
||||
components: [
|
||||
{
|
||||
component: this.pathInputBox,
|
||||
title: loc.hdfsPath,
|
||||
required: true
|
||||
}
|
||||
],
|
||||
title: loc.mountConfiguration
|
||||
}];
|
||||
}
|
||||
|
||||
protected async validate(): Promise<{ validated: boolean }> {
|
||||
try {
|
||||
await this.model.onComplete({
|
||||
url: this.urlInputBox && this.urlInputBox.value,
|
||||
auth: this.authValue,
|
||||
username: this.usernameInputBox && this.usernameInputBox.value,
|
||||
password: this.passwordInputBox && this.passwordInputBox.value,
|
||||
hdfsPath: this.pathInputBox && this.pathInputBox.value
|
||||
});
|
||||
return { validated: true };
|
||||
} catch (error) {
|
||||
await this.reportError(error);
|
||||
return { validated: false };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class RefreshMountModel extends HdfsDialogModelBase<MountHdfsProperties, void> {
|
||||
|
||||
constructor(props: MountHdfsProperties) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
protected async handleCompleted(): Promise<void> {
|
||||
this.throwIfMissingUsernamePassword();
|
||||
|
||||
// We pre-fetch the endpoints here to verify that the information entered is correct (the user is able to connect)
|
||||
let controller = await this.createAndVerifyControllerConnection();
|
||||
if (this._canceled) {
|
||||
return;
|
||||
}
|
||||
azdata.tasks.startBackgroundOperation(
|
||||
{
|
||||
connection: undefined,
|
||||
displayName: loc.refreshMountTask(this.props.hdfsPath),
|
||||
description: '',
|
||||
isCancelable: false,
|
||||
operation: op => {
|
||||
this.onSubmit(controller, op);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async onSubmit(controller: ClusterController, op: azdata.BackgroundOperation): Promise<void> {
|
||||
try {
|
||||
await controller.refreshMount(this.props.hdfsPath);
|
||||
op.updateStatus(azdata.TaskStatus.Succeeded, loc.refreshMountTaskSubmitted);
|
||||
} catch (error) {
|
||||
const errMsg = (error instanceof Error) ? error.message : error;
|
||||
vscode.window.showErrorMessage(errMsg);
|
||||
op.updateStatus(azdata.TaskStatus.Failed, errMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteMountDialog extends HdfsDialogBase<MountHdfsProperties, void> {
|
||||
private pathInputBox: azdata.InputBoxComponent;
|
||||
|
||||
constructor(model: DeleteMountModel) {
|
||||
super(loc.deleteMount, model);
|
||||
}
|
||||
|
||||
protected getMainSectionComponents(): (azdata.FormComponentGroup | azdata.FormComponent)[] {
|
||||
this.pathInputBox = this.uiModelBuilder.inputBox()
|
||||
.withProps({
|
||||
value: this.model.props.hdfsPath
|
||||
}).component();
|
||||
return [
|
||||
{
|
||||
components: [
|
||||
{
|
||||
component: this.pathInputBox,
|
||||
title: loc.hdfsPath,
|
||||
required: true
|
||||
}
|
||||
],
|
||||
title: loc.mountConfiguration
|
||||
}];
|
||||
}
|
||||
|
||||
protected async validate(): Promise<{ validated: boolean }> {
|
||||
try {
|
||||
await this.model.onComplete({
|
||||
url: this.urlInputBox && this.urlInputBox.value,
|
||||
auth: this.authValue,
|
||||
username: this.usernameInputBox && this.usernameInputBox.value,
|
||||
password: this.passwordInputBox && this.passwordInputBox.value,
|
||||
hdfsPath: this.pathInputBox && this.pathInputBox.value
|
||||
});
|
||||
return { validated: true };
|
||||
} catch (error) {
|
||||
await this.reportError(error);
|
||||
return { validated: false };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class DeleteMountModel extends HdfsDialogModelBase<MountHdfsProperties, void> {
|
||||
|
||||
constructor(props: MountHdfsProperties) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
protected async handleCompleted(): Promise<void> {
|
||||
this.throwIfMissingUsernamePassword();
|
||||
|
||||
// We pre-fetch the endpoints here to verify that the information entered is correct (the user is able to connect)
|
||||
let controller = await this.createAndVerifyControllerConnection();
|
||||
if (this._canceled) {
|
||||
return;
|
||||
}
|
||||
azdata.tasks.startBackgroundOperation(
|
||||
{
|
||||
connection: undefined,
|
||||
displayName: loc.deleteMountTask(this.props.hdfsPath),
|
||||
description: '',
|
||||
isCancelable: false,
|
||||
operation: op => {
|
||||
this.onSubmit(controller, op);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async onSubmit(controller: ClusterController, op: azdata.BackgroundOperation): Promise<void> {
|
||||
try {
|
||||
await controller.deleteMount(this.props.hdfsPath);
|
||||
op.updateStatus(azdata.TaskStatus.Succeeded, loc.deleteMountTaskSubmitted);
|
||||
} catch (error) {
|
||||
const errMsg = (error instanceof Error) ? error.message : error;
|
||||
vscode.window.showErrorMessage(errMsg);
|
||||
op.updateStatus(azdata.TaskStatus.Failed, errMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 nls from 'vscode-nls';
|
||||
import { ControllerError } from './controller/clusterControllerApi';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
// Labels
|
||||
export const statusIcon = localize('bdc.dashboard.status', "Status Icon");
|
||||
export const instance = localize('bdc.dashboard.instance', "Instance");
|
||||
export const state = localize('bdc.dashboard.state', "State");
|
||||
export const view = localize('bdc.dashboard.view', "View");
|
||||
export const notAvailable = localize('bdc.dashboard.notAvailable', "N/A");
|
||||
export const healthStatusDetails = localize('bdc.dashboard.healthStatusDetails', "Health Status Details");
|
||||
export const metricsAndLogs = localize('bdc.dashboard.metricsAndLogs', "Metrics and Logs");
|
||||
export const healthStatus = localize('bdc.dashboard.healthStatus', "Health Status");
|
||||
export const nodeMetrics = localize('bdc.dashboard.nodeMetrics', "Node Metrics");
|
||||
export const sqlMetrics = localize('bdc.dashboard.sqlMetrics', "SQL Metrics");
|
||||
export const logs = localize('bdc.dashboard.logs', "Logs");
|
||||
export function viewNodeMetrics(uri: string): string { return localize('bdc.dashboard.viewNodeMetrics', "View Node Metrics {0}", uri); }
|
||||
export function viewSqlMetrics(uri: string): string { return localize('bdc.dashboard.viewSqlMetrics', "View SQL Metrics {0}", uri); }
|
||||
export function viewLogs(uri: string): string { return localize('bdc.dashboard.viewLogs', "View Kibana Logs {0}", uri); }
|
||||
export function lastUpdated(date?: Date): string {
|
||||
return localize('bdc.dashboard.lastUpdated', "Last Updated : {0}",
|
||||
date ?
|
||||
`${date.toLocaleDateString()} ${date.toLocaleTimeString()}`
|
||||
: '-');
|
||||
}
|
||||
export const basic = localize('basicAuthName', "Basic");
|
||||
export const windowsAuth = localize('integratedAuthName', "Windows Authentication");
|
||||
export const addNewController = localize('addNewController', "Add New Controller");
|
||||
export const url = localize('url', "URL");
|
||||
export const username = localize('username', "Username");
|
||||
export const password = localize('password', "Password");
|
||||
export const rememberPassword = localize('rememberPassword', "Remember Password");
|
||||
export const clusterUrl = localize('clusterManagementUrl', "Cluster Management URL");
|
||||
export const authType = localize('textAuthCapital', "Authentication type");
|
||||
export const clusterConnection = localize('hdsf.dialog.connection.section', "Cluster Connection");
|
||||
export const add = localize('add', "Add");
|
||||
export const cancel = localize('cancel', "Cancel");
|
||||
export const ok = localize('ok', "OK");
|
||||
export const refresh = localize('bdc.dashboard.refresh', "Refresh");
|
||||
export const troubleshoot = localize('bdc.dashboard.troubleshoot', "Troubleshoot");
|
||||
export const bdcOverview = localize('bdc.dashboard.bdcOverview', "Big Data Cluster overview");
|
||||
export const clusterDetails = localize('bdc.dashboard.clusterDetails', "Cluster Details");
|
||||
export const clusterOverview = localize('bdc.dashboard.clusterOverview', "Cluster Overview");
|
||||
export const serviceEndpoints = localize('bdc.dashboard.serviceEndpoints', "Service Endpoints");
|
||||
export const clusterProperties = localize('bdc.dashboard.clusterProperties', "Cluster Properties");
|
||||
export const clusterState = localize('bdc.dashboard.clusterState', "Cluster State");
|
||||
export const serviceName = localize('bdc.dashboard.serviceName', "Service Name");
|
||||
export const service = localize('bdc.dashboard.service', "Service");
|
||||
export const endpoint = localize('bdc.dashboard.endpoint', "Endpoint");
|
||||
export function copiedEndpoint(endpointName: string): string { return localize('copiedEndpoint', "Endpoint '{0}' copied to clipboard", endpointName); }
|
||||
export const copy = localize('bdc.dashboard.copy', "Copy");
|
||||
export const viewDetails = localize('bdc.dashboard.viewDetails', "View Details");
|
||||
export const viewErrorDetails = localize('bdc.dashboard.viewErrorDetails', "View Error Details");
|
||||
export const connectToController = localize('connectController.dialog.title', "Connect to Controller");
|
||||
export const mountConfiguration = localize('mount.main.section', "Mount Configuration");
|
||||
export function mountTask(path: string): string { return localize('mount.task.name', "Mounting HDFS folder on path {0}", path); }
|
||||
export function refreshMountTask(path: string): string { return localize('refreshmount.task.name', "Refreshing HDFS Mount on path {0}", path); }
|
||||
export function deleteMountTask(path: string): string { return localize('deletemount.task.name', "Deleting HDFS Mount on path {0}", path); }
|
||||
export const mountTaskSubmitted = localize('mount.task.submitted', "Mount creation has started");
|
||||
export const refreshMountTaskSubmitted = localize('refreshmount.task.submitted', "Refresh mount request submitted");
|
||||
export const deleteMountTaskSubmitted = localize('deletemount.task.submitted', "Delete mount request submitted");
|
||||
export const mountCompleted = localize('mount.task.complete', "Mounting HDFS folder is complete");
|
||||
export const mountInProgress = localize('mount.task.inprogress', "Mounting is likely to complete, check back later to verify");
|
||||
export const mountFolder = localize('mount.dialog.title', "Mount HDFS Folder");
|
||||
export const hdfsPath = localize('mount.hdfsPath.title', "HDFS Path");
|
||||
export const hdfsPathInfo = localize('mount.hdfsPath.info', "Path to a new (non-existing) directory which you want to associate with the mount");
|
||||
export const remoteUri = localize('mount.remoteUri.title', "Remote URI");
|
||||
export const remoteUriInfo = localize('mount.remoteUri.info', "The URI to the remote data source. Example for ADLS: abfs://fs@saccount.dfs.core.windows.net/");
|
||||
export const credentials = localize('mount.credentials.title', "Credentials");
|
||||
export const credentialsInfo = localize('mount.credentials.info', "Mount credentials for authentication to remote data source for reads");
|
||||
export const refreshMount = localize('refreshmount.dialog.title', "Refresh Mount");
|
||||
export const deleteMount = localize('deleteMount.dialog.title', "Delete Mount");
|
||||
export const loadingClusterStateCompleted = localize('bdc.dashboard.loadingClusterStateCompleted', "Loading cluster state completed");
|
||||
export const loadingHealthStatusCompleted = localize('bdc.dashboard.loadingHealthStatusCompleted', "Loading health status completed");
|
||||
|
||||
// Errors
|
||||
export const usernameRequired = localize('err.controller.username.required', "Username is required");
|
||||
export const passwordRequired = localize('err.controller.password.required', "Password is required");
|
||||
export function endpointsError(msg: string): string { return localize('endpointsError', "Unexpected error retrieving BDC Endpoints: {0}", msg); }
|
||||
export const noConnectionError = localize('bdc.dashboard.noConnection', "The dashboard requires a connection. Please click retry to enter your credentials.");
|
||||
export function unexpectedError(error: Error): string { return localize('bdc.dashboard.unexpectedError', "Unexpected error occurred: {0}", error.message); }
|
||||
export const loginFailed = localize('mount.hdfs.loginerror1', "Login to controller failed");
|
||||
export function loginFailedWithError(error: ControllerError): string { return localize('mount.hdfs.loginerror2', "Login to controller failed: {0}", error.statusMessage || error.message); }
|
||||
export function badCredentialsFormatting(pair: string): string { return localize('mount.err.formatting', "Bad formatting of credentials at {0}", pair); }
|
||||
export function mountError(error: any): string { return localize('mount.task.error', "Error mounting folder: {0}", (error instanceof Error ? error.message : error)); }
|
||||
export const mountErrorUnknown = localize('mount.error.unknown', "Unknown error occurred during the mount process");
|
||||
@@ -0,0 +1,10 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { TreeNode } from './treeNode';
|
||||
|
||||
export interface IControllerTreeChangeHandler {
|
||||
notifyNodeChanged(node?: TreeNode): void;
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 nls from 'vscode-nls';
|
||||
import { TreeNode } from './treeNode';
|
||||
import { IControllerTreeChangeHandler } from './controllerTreeChangeHandler';
|
||||
import { ControllerRootNode, ControllerNode } from './controllerTreeNode';
|
||||
import { showErrorMessage } from '../utils';
|
||||
import { AuthType } from 'bdc';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
const CredentialNamespace = 'clusterControllerCredentials';
|
||||
|
||||
interface IControllerInfoSlim {
|
||||
url: string;
|
||||
auth: AuthType;
|
||||
username: string;
|
||||
password?: string;
|
||||
rememberPassword: boolean;
|
||||
}
|
||||
|
||||
export class ControllerTreeDataProvider implements vscode.TreeDataProvider<TreeNode>, IControllerTreeChangeHandler {
|
||||
|
||||
private _onDidChangeTreeData: vscode.EventEmitter<TreeNode> = new vscode.EventEmitter<TreeNode>();
|
||||
public readonly onDidChangeTreeData: vscode.Event<TreeNode> = this._onDidChangeTreeData.event;
|
||||
private root: ControllerRootNode;
|
||||
private credentialProvider: azdata.CredentialProvider;
|
||||
private initialized: boolean = false;
|
||||
|
||||
constructor(private memento: vscode.Memento) {
|
||||
this.root = new ControllerRootNode(this);
|
||||
}
|
||||
|
||||
public async getChildren(element?: TreeNode): Promise<TreeNode[]> {
|
||||
if (element) {
|
||||
return element.getChildren();
|
||||
}
|
||||
|
||||
if (!this.initialized) {
|
||||
try {
|
||||
await this.loadSavedControllers();
|
||||
} catch (err) {
|
||||
void vscode.window.showErrorMessage(localize('bdc.controllerTreeDataProvider.error', "Unexpected error loading saved controllers: {0}", err));
|
||||
}
|
||||
}
|
||||
|
||||
return this.root.getChildren();
|
||||
}
|
||||
|
||||
public getTreeItem(element: TreeNode): vscode.TreeItem | Thenable<vscode.TreeItem> {
|
||||
return element.getTreeItem();
|
||||
}
|
||||
|
||||
public notifyNodeChanged(node?: TreeNode): void {
|
||||
this._onDidChangeTreeData.fire(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or updates a node in the tree with the specified connection information
|
||||
* @param url The URL for the BDC management endpoint
|
||||
* @param auth The type of auth to use
|
||||
* @param username The username (if basic auth)
|
||||
* @param password The password (if basic auth)
|
||||
* @param rememberPassword Whether to store the password in the password store when saving
|
||||
*/
|
||||
public addOrUpdateController(
|
||||
url: string,
|
||||
auth: AuthType,
|
||||
username: string,
|
||||
password: string,
|
||||
rememberPassword: boolean
|
||||
): void {
|
||||
this.removeNonControllerNodes();
|
||||
this.root.addOrUpdateControllerNode(url, auth, username, password, rememberPassword);
|
||||
this.notifyNodeChanged();
|
||||
}
|
||||
|
||||
public removeController(url: string, auth: AuthType, username: string): ControllerNode[] {
|
||||
let removed = this.root.removeControllerNode(url, auth, username);
|
||||
if (removed) {
|
||||
this.notifyNodeChanged();
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
private removeNonControllerNodes(): void {
|
||||
this.removeDefectiveControllerNodes(this.root.children);
|
||||
}
|
||||
|
||||
private removeDefectiveControllerNodes(nodes: TreeNode[]): void {
|
||||
if (nodes.length > 0) {
|
||||
for (let i = 0; i < nodes.length; ++i) {
|
||||
if (nodes[i] instanceof ControllerNode) {
|
||||
let controller = nodes[i] as ControllerNode;
|
||||
if (!controller.url || !controller.id) {
|
||||
nodes.splice(i--, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async loadSavedControllers(): Promise<void> {
|
||||
// Optimistically set to true so we don't double-load the tree
|
||||
this.initialized = true;
|
||||
try {
|
||||
let controllers: IControllerInfoSlim[] = this.memento.get('controllers');
|
||||
let treeNodes: TreeNode[] = [];
|
||||
if (controllers) {
|
||||
for (const c of controllers) {
|
||||
let password = undefined;
|
||||
if (c.rememberPassword) {
|
||||
password = await this.getPassword(c.url, c.username);
|
||||
}
|
||||
if (!c.auth) {
|
||||
// Added before we had added authentication
|
||||
c.auth = 'basic';
|
||||
}
|
||||
treeNodes.push(new ControllerNode(
|
||||
c.url, c.auth, c.username, password, c.rememberPassword,
|
||||
undefined, this.root, this, undefined
|
||||
));
|
||||
}
|
||||
this.removeDefectiveControllerNodes(treeNodes);
|
||||
}
|
||||
|
||||
this.root.clearChildren();
|
||||
treeNodes.forEach(node => this.root.addChild(node));
|
||||
await vscode.commands.executeCommand('setContext', 'bdc.loaded', true);
|
||||
} catch (err) {
|
||||
// Reset so we can try again if the tree refreshes
|
||||
this.initialized = false;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
public async saveControllers(): Promise<void> {
|
||||
const controllers = this.root.children.map((e): IControllerInfoSlim => {
|
||||
const controller = e as ControllerNode;
|
||||
return {
|
||||
url: controller.url,
|
||||
auth: controller.auth,
|
||||
username: controller.username,
|
||||
password: controller.password,
|
||||
rememberPassword: controller.rememberPassword
|
||||
};
|
||||
});
|
||||
|
||||
const controllersWithoutPassword = controllers.map((e): IControllerInfoSlim => {
|
||||
return {
|
||||
url: e.url,
|
||||
auth: e.auth,
|
||||
username: e.username,
|
||||
rememberPassword: e.rememberPassword
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
await this.memento.update('controllers', controllersWithoutPassword);
|
||||
} catch (error) {
|
||||
showErrorMessage(error);
|
||||
}
|
||||
|
||||
for (const e of controllers) {
|
||||
if (e.rememberPassword) {
|
||||
await this.savePassword(e.url, e.username, e.password);
|
||||
} else {
|
||||
await this.deletePassword(e.url, e.username);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async savePassword(url: string, username: string, password: string): Promise<boolean> {
|
||||
let provider = await this.getCredentialProvider();
|
||||
let id = this.createId(url, username);
|
||||
let result = await provider.saveCredential(id, password);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async deletePassword(url: string, username: string): Promise<boolean> {
|
||||
let provider = await this.getCredentialProvider();
|
||||
let id = this.createId(url, username);
|
||||
let result = await provider.deleteCredential(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async getPassword(url: string, username: string): Promise<string | undefined> {
|
||||
let provider = await this.getCredentialProvider();
|
||||
let id = this.createId(url, username);
|
||||
let credential = await provider.readCredential(id);
|
||||
return credential ? credential.password : undefined;
|
||||
}
|
||||
|
||||
private async getCredentialProvider(): Promise<azdata.CredentialProvider> {
|
||||
if (!this.credentialProvider) {
|
||||
this.credentialProvider = await azdata.credentials.getProvider(CredentialNamespace);
|
||||
}
|
||||
return this.credentialProvider;
|
||||
}
|
||||
|
||||
private createId(url: string, username: string): string {
|
||||
return `${url}::${username}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { IControllerTreeChangeHandler } from './controllerTreeChangeHandler';
|
||||
import { TreeNode } from './treeNode';
|
||||
import { IconPathHelper, BdcItemType, IconPath } from '../constants';
|
||||
import { AuthType } from 'bdc';
|
||||
|
||||
abstract class ControllerTreeNode extends TreeNode {
|
||||
|
||||
constructor(
|
||||
label: string,
|
||||
parent: ControllerTreeNode,
|
||||
private _treeChangeHandler: IControllerTreeChangeHandler,
|
||||
private _description?: string,
|
||||
private _nodeType?: string,
|
||||
private _iconPath?: IconPath
|
||||
) {
|
||||
super(label, parent);
|
||||
this._description = this._description || this.label;
|
||||
}
|
||||
|
||||
public async getChildren(): Promise<ControllerTreeNode[]> {
|
||||
return this.children as ControllerTreeNode[];
|
||||
}
|
||||
|
||||
public override refresh(): void {
|
||||
super.refresh();
|
||||
this.treeChangeHandler.notifyNodeChanged(this);
|
||||
}
|
||||
|
||||
public getTreeItem(): vscode.TreeItem {
|
||||
let item: vscode.TreeItem = {};
|
||||
item.id = this.id;
|
||||
item.label = this.label;
|
||||
item.collapsibleState = vscode.TreeItemCollapsibleState.None;
|
||||
item.iconPath = this._iconPath;
|
||||
item.contextValue = this._nodeType;
|
||||
item.tooltip = this._description;
|
||||
item.iconPath = this._iconPath;
|
||||
return item;
|
||||
}
|
||||
|
||||
public getNodeInfo(): azdata.NodeInfo {
|
||||
return {
|
||||
label: this.label,
|
||||
isLeaf: this.isLeaf,
|
||||
errorMessage: undefined,
|
||||
metadata: undefined,
|
||||
nodePath: this.nodePath,
|
||||
nodeStatus: undefined,
|
||||
nodeType: this._nodeType,
|
||||
iconType: this._nodeType,
|
||||
nodeSubType: undefined
|
||||
};
|
||||
}
|
||||
|
||||
public get description(): string {
|
||||
return this._description;
|
||||
}
|
||||
|
||||
public set description(description: string) {
|
||||
this._description = description;
|
||||
}
|
||||
|
||||
public get nodeType(): string {
|
||||
return this._nodeType;
|
||||
}
|
||||
|
||||
public set nodeType(nodeType: string) {
|
||||
this._nodeType = nodeType;
|
||||
}
|
||||
|
||||
public set iconPath(iconPath: IconPath) {
|
||||
this._iconPath = iconPath;
|
||||
}
|
||||
|
||||
public get iconPath(): IconPath {
|
||||
return this._iconPath;
|
||||
}
|
||||
|
||||
public set treeChangeHandler(treeChangeHandler: IControllerTreeChangeHandler) {
|
||||
this._treeChangeHandler = treeChangeHandler;
|
||||
}
|
||||
|
||||
public get treeChangeHandler(): IControllerTreeChangeHandler {
|
||||
return this._treeChangeHandler;
|
||||
}
|
||||
}
|
||||
|
||||
export class ControllerRootNode extends ControllerTreeNode {
|
||||
|
||||
constructor(treeChangeHandler: IControllerTreeChangeHandler) {
|
||||
super('root', undefined, treeChangeHandler, undefined, BdcItemType.controllerRoot);
|
||||
}
|
||||
|
||||
public override async getChildren(): Promise<ControllerNode[]> {
|
||||
return this.children as ControllerNode[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or updates a node in the tree with the specified connection information
|
||||
* @param url The URL for the BDC management endpoint
|
||||
* @param auth The type of auth to use
|
||||
* @param username The username (if basic auth)
|
||||
* @param password The password (if basic auth)
|
||||
* @param rememberPassword Whether to store the password in the password store when saving
|
||||
*/
|
||||
public addOrUpdateControllerNode(
|
||||
url: string,
|
||||
auth: AuthType,
|
||||
username: string,
|
||||
password: string,
|
||||
rememberPassword: boolean
|
||||
): void {
|
||||
let controllerNode = this.getExistingControllerNode(url, auth, username);
|
||||
if (controllerNode) {
|
||||
controllerNode.password = password;
|
||||
controllerNode.rememberPassword = rememberPassword;
|
||||
controllerNode.clearChildren();
|
||||
} else {
|
||||
controllerNode = new ControllerNode(url, auth, username, password, rememberPassword, undefined, this, this.treeChangeHandler, undefined);
|
||||
this.addChild(controllerNode);
|
||||
}
|
||||
}
|
||||
|
||||
public removeControllerNode(url: string, auth: AuthType, username: string): ControllerNode[] | undefined {
|
||||
if (!url || (auth === 'basic' && !username)) {
|
||||
return undefined;
|
||||
}
|
||||
let nodes = this.children as ControllerNode[];
|
||||
let index = nodes.findIndex(e => isControllerMatch(e, url, auth, username));
|
||||
let deleted: ControllerNode[] | undefined;
|
||||
if (index >= 0) {
|
||||
deleted = nodes.splice(index, 1);
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
private getExistingControllerNode(url: string, auth: AuthType, username: string): ControllerNode | undefined {
|
||||
if (!url || !username) {
|
||||
return undefined;
|
||||
}
|
||||
let nodes = this.children as ControllerNode[];
|
||||
return nodes.find(e => isControllerMatch(e, url, auth, username));
|
||||
}
|
||||
}
|
||||
|
||||
export class ControllerNode extends ControllerTreeNode {
|
||||
|
||||
constructor(
|
||||
private _url: string,
|
||||
private _auth: AuthType,
|
||||
private _username: string,
|
||||
private _password: string,
|
||||
private _rememberPassword: boolean,
|
||||
label: string,
|
||||
parent: ControllerTreeNode,
|
||||
treeChangeHandler: IControllerTreeChangeHandler,
|
||||
description?: string,
|
||||
) {
|
||||
super(label, parent, treeChangeHandler, description, BdcItemType.controller, IconPathHelper.controllerNode);
|
||||
this.label = label;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public override async getChildren(): Promise<ControllerTreeNode[] | undefined> {
|
||||
if (this.children && this.children.length > 0) {
|
||||
this.clearChildren();
|
||||
}
|
||||
|
||||
if (!this._password) {
|
||||
vscode.commands.executeCommand('bigDataClusters.command.connectController', this);
|
||||
return this.children as ControllerTreeNode[];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public static toIpAndPort(url: string): string | undefined {
|
||||
if (!url) {
|
||||
return undefined;
|
||||
}
|
||||
return url.trim().replace(/ /g, '').replace(/^.+\:\/\//, '');
|
||||
}
|
||||
|
||||
public get url(): string {
|
||||
return this._url;
|
||||
}
|
||||
|
||||
|
||||
public get auth(): AuthType {
|
||||
return this._auth;
|
||||
}
|
||||
|
||||
|
||||
public get username(): string {
|
||||
return this._username;
|
||||
}
|
||||
|
||||
public get password(): string {
|
||||
return this._password;
|
||||
}
|
||||
|
||||
public set password(pw: string) {
|
||||
this._password = pw;
|
||||
}
|
||||
|
||||
public override set label(label: string) {
|
||||
super.label = label || this.generateLabel();
|
||||
}
|
||||
|
||||
public get rememberPassword() {
|
||||
return this._rememberPassword;
|
||||
}
|
||||
|
||||
public set rememberPassword(rememberPassword: boolean) {
|
||||
this._rememberPassword = rememberPassword;
|
||||
}
|
||||
|
||||
private generateLabel(): string {
|
||||
let label = `controller: ${ControllerNode.toIpAndPort(this._url)}`;
|
||||
if (this._auth === 'basic') {
|
||||
label += ` (${this._username})`;
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
public override get label(): string {
|
||||
return super.label;
|
||||
}
|
||||
|
||||
public override set description(description: string) {
|
||||
super.description = description || super.label;
|
||||
}
|
||||
|
||||
public override get description(): string {
|
||||
return super.description;
|
||||
}
|
||||
}
|
||||
|
||||
function isControllerMatch(node: ControllerNode, url: string, auth: string, username: string): unknown {
|
||||
return node.url === url && node.auth === auth && node.username === username;
|
||||
}
|
||||
193
extensions/big-data-cluster/src/bigDataCluster/tree/treeNode.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { generateGuid } from '../utils';
|
||||
|
||||
export abstract class TreeNode {
|
||||
|
||||
private _id: string;
|
||||
private _children: TreeNode[];
|
||||
private _isLeaf: boolean;
|
||||
|
||||
constructor(private _label: string, private _parent?: TreeNode) {
|
||||
this.resetId();
|
||||
}
|
||||
|
||||
public resetId(): void {
|
||||
this._id = (this._label || '_') + `::${generateGuid()}`;
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
public set label(label: string) {
|
||||
if (!this._label) {
|
||||
this._label = label;
|
||||
this.resetId();
|
||||
} else {
|
||||
this._label = label;
|
||||
}
|
||||
}
|
||||
|
||||
public get label(): string {
|
||||
return this._label;
|
||||
}
|
||||
|
||||
public set parent(parent: TreeNode) {
|
||||
this._parent = parent;
|
||||
}
|
||||
|
||||
public get parent(): TreeNode {
|
||||
return this._parent;
|
||||
}
|
||||
|
||||
public get children(): TreeNode[] {
|
||||
if (!this._children) {
|
||||
this._children = [];
|
||||
}
|
||||
return this._children;
|
||||
}
|
||||
|
||||
public get hasChildren(): boolean {
|
||||
return this.children && this.children.length > 0;
|
||||
}
|
||||
|
||||
public set isLeaf(isLeaf: boolean) {
|
||||
this._isLeaf = isLeaf;
|
||||
}
|
||||
|
||||
public get isLeaf(): boolean {
|
||||
return this._isLeaf;
|
||||
}
|
||||
|
||||
public get root(): TreeNode {
|
||||
return TreeNode.getRoot(this);
|
||||
}
|
||||
|
||||
public equals(node: TreeNode): boolean {
|
||||
if (!node) {
|
||||
return undefined;
|
||||
}
|
||||
return this.nodePath === node.nodePath;
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
this.resetId();
|
||||
}
|
||||
|
||||
public static getRoot(node: TreeNode): TreeNode {
|
||||
if (!node) {
|
||||
return undefined;
|
||||
}
|
||||
let current: TreeNode = node;
|
||||
while (current.parent) {
|
||||
current = current.parent;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
public get nodePath(): string {
|
||||
return TreeNode.getNodePath(this);
|
||||
}
|
||||
|
||||
public static getNodePath(node: TreeNode): string {
|
||||
if (!node) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let current: TreeNode = node;
|
||||
let path = current._id;
|
||||
while (current.parent) {
|
||||
current = current.parent;
|
||||
path = `${current._id}/${path}`;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
public async findNode(condition: (node: TreeNode) => boolean, expandIfNeeded?: boolean): Promise<TreeNode> {
|
||||
return TreeNode.findNode(this, condition, expandIfNeeded);
|
||||
}
|
||||
|
||||
public static async findNode(node: TreeNode, condition: (node: TreeNode) => boolean, expandIfNeeded?: boolean): Promise<TreeNode> {
|
||||
if (!node || !condition) {
|
||||
return undefined;
|
||||
}
|
||||
let result: TreeNode = undefined;
|
||||
let nodesToCheck: TreeNode[] = [node];
|
||||
while (nodesToCheck.length > 0) {
|
||||
let current = nodesToCheck.shift();
|
||||
if (condition(current)) {
|
||||
result = current;
|
||||
break;
|
||||
}
|
||||
if (current.hasChildren) {
|
||||
nodesToCheck = nodesToCheck.concat(current.children);
|
||||
} else if (expandIfNeeded) {
|
||||
let children = await current.getChildren();
|
||||
if (children && children.length > 0) {
|
||||
nodesToCheck = nodesToCheck.concat(children);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async filterNode(condition: (node: TreeNode) => boolean, expandIfNeeded?: boolean): Promise<TreeNode[]> {
|
||||
return TreeNode.filterNode(this, condition, expandIfNeeded);
|
||||
}
|
||||
|
||||
public static async filterNode(node: TreeNode, condition: (node: TreeNode) => boolean, expandIfNeeded?: boolean): Promise<TreeNode[]> {
|
||||
if (!node || !condition) {
|
||||
return undefined;
|
||||
}
|
||||
let result: TreeNode[] = [];
|
||||
let nodesToCheck: TreeNode[] = [node];
|
||||
while (nodesToCheck.length > 0) {
|
||||
let current = nodesToCheck.shift();
|
||||
if (condition(current)) {
|
||||
result.push(current);
|
||||
}
|
||||
if (current.hasChildren) {
|
||||
nodesToCheck = nodesToCheck.concat(current.children);
|
||||
} else if (expandIfNeeded) {
|
||||
let children = await current.getChildren();
|
||||
if (children && children.length > 0) {
|
||||
nodesToCheck = nodesToCheck.concat(children);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async findNodeByPath(path: string, expandIfNeeded?: boolean): Promise<TreeNode> {
|
||||
return TreeNode.findNodeByPath(this, path, expandIfNeeded);
|
||||
}
|
||||
|
||||
public static async findNodeByPath(node: TreeNode, path: string, expandIfNeeded?: boolean): Promise<TreeNode> {
|
||||
return TreeNode.findNode(node, node => {
|
||||
return node.nodePath && (node.nodePath === path || node.nodePath.startsWith(path));
|
||||
}, expandIfNeeded);
|
||||
}
|
||||
|
||||
public addChild(node: TreeNode): void {
|
||||
if (!this._children) {
|
||||
this._children = [];
|
||||
}
|
||||
this._children.push(node);
|
||||
}
|
||||
|
||||
public clearChildren(): void {
|
||||
if (this._children) {
|
||||
this._children = [];
|
||||
}
|
||||
}
|
||||
|
||||
public abstract getChildren(): Promise<TreeNode[]>;
|
||||
public abstract getTreeItem(): vscode.TreeItem;
|
||||
public abstract getNodeInfo(): azdata.NodeInfo;
|
||||
}
|
||||
289
extensions/big-data-cluster/src/bigDataCluster/utils.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 nls from 'vscode-nls';
|
||||
import * as constants from './constants';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export enum Endpoint {
|
||||
gateway = 'gateway',
|
||||
sparkHistory = 'spark-history',
|
||||
yarnUi = 'yarn-ui',
|
||||
appProxy = 'app-proxy',
|
||||
mgmtproxy = 'mgmtproxy',
|
||||
managementProxy = 'management-proxy',
|
||||
logsui = 'logsui',
|
||||
metricsui = 'metricsui',
|
||||
controller = 'controller',
|
||||
sqlServerMaster = 'sql-server-master',
|
||||
webhdfs = 'webhdfs',
|
||||
livy = 'livy'
|
||||
}
|
||||
|
||||
export enum Service {
|
||||
sql = 'sql',
|
||||
hdfs = 'hdfs',
|
||||
spark = 'spark',
|
||||
control = 'control',
|
||||
gateway = 'gateway',
|
||||
app = 'app'
|
||||
}
|
||||
|
||||
export function generateGuid(): string {
|
||||
let hexValues: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
|
||||
let oct: string = '';
|
||||
let tmp: number;
|
||||
for (let a: number = 0; a < 4; a++) {
|
||||
tmp = (4294967296 * Math.random()) | 0;
|
||||
oct += hexValues[tmp & 0xF] +
|
||||
hexValues[tmp >> 4 & 0xF] +
|
||||
hexValues[tmp >> 8 & 0xF] +
|
||||
hexValues[tmp >> 12 & 0xF] +
|
||||
hexValues[tmp >> 16 & 0xF] +
|
||||
hexValues[tmp >> 20 & 0xF] +
|
||||
hexValues[tmp >> 24 & 0xF] +
|
||||
hexValues[tmp >> 28 & 0xF];
|
||||
}
|
||||
let clockSequenceHi: string = hexValues[8 + (Math.random() * 4) | 0];
|
||||
return oct.substr(0, 8) + '-' + oct.substr(9, 4) + '-4' + oct.substr(13, 3) + '-' + clockSequenceHi + oct.substr(16, 3) + '-' + oct.substr(19, 12);
|
||||
}
|
||||
|
||||
export function showErrorMessage(error: any, prefixText?: string): void {
|
||||
if (error) {
|
||||
let text: string = prefixText || '';
|
||||
if (typeof error === 'string') {
|
||||
text += error as string;
|
||||
} else if (typeof error === 'object' && error !== null) {
|
||||
text += error.message;
|
||||
if (error.code && error.code > 0) {
|
||||
text += ` (${error.code})`;
|
||||
}
|
||||
} else {
|
||||
text += `${error}`;
|
||||
}
|
||||
vscode.window.showErrorMessage(text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappings of the different expected state values to their localized friendly names.
|
||||
* These are defined in aris/projects/controller/src/Microsoft.SqlServer.Controller/StateMachines
|
||||
*/
|
||||
const stateToDisplayTextMap: { [key: string]: string } = {
|
||||
// K8sScaledSetStateMachine
|
||||
'creating': localize('state.creating', "Creating"),
|
||||
'waiting': localize('state.waiting', "Waiting"),
|
||||
'ready': localize('state.ready', "Ready"),
|
||||
'deleting': localize('state.deleting', "Deleting"),
|
||||
'deleted': localize('state.deleted', "Deleted"),
|
||||
'applyingupgrade': localize('state.applyingUpgrade', "Applying Upgrade"),
|
||||
'upgrading': localize('state.upgrading', "Upgrading"),
|
||||
'applyingmanagedupgrade': localize('state.applyingmanagedupgrade', "Applying Managed Upgrade"),
|
||||
'managedupgrading': localize('state.managedUpgrading', "Managed Upgrading"),
|
||||
'rollback': localize('state.rollback', "Rollback"),
|
||||
'rollbackinprogress': localize('state.rollbackInProgress', "Rollback In Progress"),
|
||||
'rollbackcomplete': localize('state.rollbackComplete', "Rollback Complete"),
|
||||
'error': localize('state.error', "Error"),
|
||||
|
||||
// BigDataClusterStateMachine
|
||||
'creatingsecrets': localize('state.creatingSecrets', "Creating Secrets"),
|
||||
'waitingforsecrets': localize('state.waitingForSecrets', "Waiting For Secrets"),
|
||||
'creatinggroups': localize('state.creatingGroups', "Creating Groups"),
|
||||
'waitingforgroups': localize('state.waitingForGroups', "Waiting For Groups"),
|
||||
'creatingresources': localize('state.creatingResources', "Creating Resources"),
|
||||
'waitingforresources': localize('state.waitingForResources', "Waiting For Resources"),
|
||||
'creatingkerberosdelegationsetup': localize('state.creatingKerberosDelegationSetup', "Creating Kerberos Delegation Setup"),
|
||||
'waitingforkerberosdelegationsetup': localize('state.waitingForKerberosDelegationSetup', "Waiting For Kerberos Delegation Setup"),
|
||||
'waitingfordeletion': localize('state.waitingForDeletion', "Waiting For Deletion"),
|
||||
'waitingforupgrade': localize('state.waitingForUpgrade', "Waiting For Upgrade"),
|
||||
'upgradePaused': localize('state.upgradePaused', "Upgrade Paused"),
|
||||
|
||||
// Other
|
||||
'running': localize('state.running', "Running"),
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the localized text to display for a corresponding state
|
||||
* @param state The state to get the display text for
|
||||
*/
|
||||
export function getStateDisplayText(state?: string): string {
|
||||
state = state || '';
|
||||
return stateToDisplayTextMap[state.toLowerCase()] || state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the localized text to display for a corresponding endpoint
|
||||
* @param endpointName The endpoint name to get the display text for
|
||||
* @param description The backup description to use if we don't have our own
|
||||
*/
|
||||
export function getEndpointDisplayText(endpointName?: string, description?: string): string {
|
||||
endpointName = endpointName || '';
|
||||
switch (endpointName.toLowerCase()) {
|
||||
case Endpoint.appProxy:
|
||||
return localize('endpoint.appproxy', "Application Proxy");
|
||||
case Endpoint.controller:
|
||||
return localize('endpoint.controller', "Cluster Management Service");
|
||||
case Endpoint.gateway:
|
||||
return localize('endpoint.gateway', "Gateway to access HDFS files, Spark");
|
||||
case Endpoint.managementProxy:
|
||||
return localize('endpoint.managementproxy', "Management Proxy");
|
||||
case Endpoint.mgmtproxy:
|
||||
return localize('endpoint.mgmtproxy', "Management Proxy");
|
||||
case Endpoint.sqlServerMaster:
|
||||
return localize('endpoint.sqlServerEndpoint', "SQL Server Master Instance Front-End");
|
||||
case Endpoint.metricsui:
|
||||
return localize('endpoint.grafana', "Metrics Dashboard");
|
||||
case Endpoint.logsui:
|
||||
return localize('endpoint.kibana', "Log Search Dashboard");
|
||||
case Endpoint.yarnUi:
|
||||
return localize('endpoint.yarnHistory', "Spark Diagnostics and Monitoring Dashboard");
|
||||
case Endpoint.sparkHistory:
|
||||
return localize('endpoint.sparkHistory', "Spark Jobs Management and Monitoring Dashboard");
|
||||
case Endpoint.webhdfs:
|
||||
return localize('endpoint.webhdfs', "HDFS File System Proxy");
|
||||
case Endpoint.livy:
|
||||
return localize('endpoint.livy', "Proxy for running Spark statements, jobs, applications");
|
||||
default:
|
||||
// Default is to use the description if one was given, otherwise worst case just fall back to using the
|
||||
// original endpoint name
|
||||
return description && description.length > 0 ? description : endpointName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the localized text to display for a corresponding service
|
||||
* @param serviceName The service name to get the display text for
|
||||
*/
|
||||
export function getServiceNameDisplayText(serviceName?: string): string {
|
||||
serviceName = serviceName || '';
|
||||
switch (serviceName.toLowerCase()) {
|
||||
case Service.sql:
|
||||
return localize('service.sql', "SQL Server");
|
||||
case Service.hdfs:
|
||||
return localize('service.hdfs', "HDFS");
|
||||
case Service.spark:
|
||||
return localize('service.spark', "Spark");
|
||||
case Service.control:
|
||||
return localize('service.control', "Control");
|
||||
case Service.gateway:
|
||||
return localize('service.gateway', "Gateway");
|
||||
case Service.app:
|
||||
return localize('service.app', "App");
|
||||
default:
|
||||
return serviceName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the localized text to display for a corresponding health status
|
||||
* @param healthStatus The health status to get the display text for
|
||||
*/
|
||||
export function getHealthStatusDisplayText(healthStatus?: string) {
|
||||
healthStatus = healthStatus || '';
|
||||
switch (healthStatus.toLowerCase()) {
|
||||
case 'healthy':
|
||||
return localize('bdc.healthy', "Healthy");
|
||||
case 'unhealthy':
|
||||
return localize('bdc.unhealthy', "Unhealthy");
|
||||
default:
|
||||
return healthStatus;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the status icon for the corresponding health status
|
||||
* @param healthStatus The status to check
|
||||
*/
|
||||
export function getHealthStatusIcon(healthStatus?: string): string {
|
||||
healthStatus = healthStatus || '';
|
||||
switch (healthStatus.toLowerCase()) {
|
||||
case 'healthy':
|
||||
return '✔️';
|
||||
default:
|
||||
// Consider all non-healthy status' as errors
|
||||
return '⚠️';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the status dot icon which will be a • for all non-healthy states
|
||||
* @param healthStatus The status to check
|
||||
*/
|
||||
export function getHealthStatusDotIcon(healthStatus?: string): constants.IconPath {
|
||||
healthStatus = healthStatus || '';
|
||||
switch (healthStatus.toLowerCase()) {
|
||||
case 'healthy':
|
||||
return constants.IconPathHelper.status_circle_blank;
|
||||
default:
|
||||
// Display status dot for all non-healthy status'
|
||||
return constants.IconPathHelper.status_circle_red;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interface RawEndpoint {
|
||||
serviceName: string;
|
||||
description?: string;
|
||||
endpoint?: string;
|
||||
protocol?: string;
|
||||
ipAddress?: string;
|
||||
port?: number;
|
||||
}
|
||||
|
||||
interface IEndpoint {
|
||||
serviceName: string;
|
||||
description: string;
|
||||
endpoint: string;
|
||||
protocol: string;
|
||||
}
|
||||
|
||||
function getClusterEndpoints(serverInfo: azdata.ServerInfo): IEndpoint[] {
|
||||
let endpoints: RawEndpoint[] = serverInfo.options[constants.clusterEndpointsProperty];
|
||||
if (!endpoints || endpoints.length === 0) { return []; }
|
||||
|
||||
return endpoints.map(e => {
|
||||
// If endpoint is missing, we're on CTP bits. All endpoints from the CTP serverInfo should be treated as HTTPS
|
||||
let endpoint = e.endpoint ? e.endpoint : `https://${e.ipAddress}:${e.port}`;
|
||||
let updatedEndpoint: IEndpoint = {
|
||||
serviceName: e.serviceName,
|
||||
description: e.description,
|
||||
endpoint: endpoint,
|
||||
protocol: e.protocol
|
||||
};
|
||||
return updatedEndpoint;
|
||||
});
|
||||
}
|
||||
|
||||
export function getControllerEndpoint(serverInfo: azdata.ServerInfo): string | undefined {
|
||||
let endpoints = getClusterEndpoints(serverInfo);
|
||||
if (endpoints) {
|
||||
let index = endpoints.findIndex(ep => ep.serviceName.toLowerCase() === constants.controllerEndpointName.toLowerCase());
|
||||
if (index < 0) { return undefined; }
|
||||
return endpoints[index].endpoint;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getBdcStatusErrorMessage(error: Error): string {
|
||||
return localize('endpointsError', "Unexpected error retrieving BDC Endpoints: {0}", error.message);
|
||||
}
|
||||
|
||||
const bdcConfigSectionName = 'bigDataCluster';
|
||||
const ignoreSslConfigName = 'ignoreSslVerification';
|
||||
|
||||
/**
|
||||
* Retrieves the current setting for whether to ignore SSL verification errors
|
||||
*/
|
||||
export function getIgnoreSslVerificationConfigSetting(): boolean {
|
||||
try {
|
||||
const config = vscode.workspace.getConfiguration(bdcConfigSectionName);
|
||||
return config.get<boolean>(ignoreSslConfigName, true);
|
||||
} catch (error) {
|
||||
console.error(`Unexpected error retrieving ${bdcConfigSectionName}.${ignoreSslConfigName} setting : ${error}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
13
extensions/big-data-cluster/src/commands.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export const ManageControllerCommand = 'bigDataClusters.command.manageController';
|
||||
export const CreateControllerCommand = 'bigDataClusters.command.createController';
|
||||
export const ConnectControllerCommand = 'bigDataClusters.command.connectController';
|
||||
export const RemoveControllerCommand = 'bigDataClusters.command.removeController';
|
||||
export const RefreshControllerCommand = 'bigDataClusters.command.refreshController';
|
||||
export const MountHdfsCommand = 'bigDataClusters.command.mount';
|
||||
export const RefreshMountCommand = 'bigDataClusters.command.refreshmount';
|
||||
export const DeleteMountCommand = 'bigDataClusters.command.deletemount';
|
||||
25
extensions/big-data-cluster/src/common/promise.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Deferred promise
|
||||
*/
|
||||
export class Deferred<T> {
|
||||
promise: Promise<T>;
|
||||
resolve: (value?: T | PromiseLike<T>) => void;
|
||||
reject: (reason?: any) => void;
|
||||
constructor() {
|
||||
this.promise = new Promise<T>((resolve, reject) => {
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
});
|
||||
}
|
||||
|
||||
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => TResult | Thenable<TResult>): Thenable<TResult>;
|
||||
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => void): Thenable<TResult>;
|
||||
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => TResult | Thenable<TResult>): Thenable<TResult> {
|
||||
return this.promise.then(onfulfilled, onrejected);
|
||||
}
|
||||
}
|
||||
225
extensions/big-data-cluster/src/extension.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 nls from 'vscode-nls';
|
||||
import { ControllerTreeDataProvider } from './bigDataCluster/tree/controllerTreeDataProvider';
|
||||
import { IconPathHelper } from './bigDataCluster/constants';
|
||||
import { TreeNode } from './bigDataCluster/tree/treeNode';
|
||||
import { AddControllerDialogModel, AddControllerDialog } from './bigDataCluster/dialog/addControllerDialog';
|
||||
import { ControllerNode } from './bigDataCluster/tree/controllerTreeNode';
|
||||
import { BdcDashboard } from './bigDataCluster/dialog/bdcDashboard';
|
||||
import { BdcDashboardModel, BdcDashboardOptions } from './bigDataCluster/dialog/bdcDashboardModel';
|
||||
import { MountHdfsDialogModel as MountHdfsModel, MountHdfsProperties, MountHdfsDialog, DeleteMountDialog, DeleteMountModel, RefreshMountDialog, RefreshMountModel } from './bigDataCluster/dialog/mountHdfsDialog';
|
||||
import { getControllerEndpoint } from './bigDataCluster/utils';
|
||||
import * as commands from './commands';
|
||||
import { HdfsDialogCancelledError } from './bigDataCluster/dialog/hdfsDialogBase';
|
||||
import { IExtension, AuthType, IClusterController } from 'bdc';
|
||||
import { ClusterController } from './bigDataCluster/controller/clusterControllerApi';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
const endpointNotFoundError = localize('mount.error.endpointNotFound', "Controller endpoint information was not found");
|
||||
|
||||
let throttleTimers: { [key: string]: any } = {};
|
||||
|
||||
export async function activate(extensionContext: vscode.ExtensionContext): Promise<IExtension> {
|
||||
IconPathHelper.setExtensionContext(extensionContext);
|
||||
await vscode.commands.executeCommand('setContext', 'bdc.loaded', false);
|
||||
const treeDataProvider = new ControllerTreeDataProvider(extensionContext.globalState);
|
||||
let controllers: any[] = extensionContext.globalState.get('controllers', []);
|
||||
if (controllers.length > 0) {
|
||||
const deprecationNoticeKey = 'bdc.deprecationNoticeShown';
|
||||
const deprecationNoticeShown = extensionContext.globalState.get(deprecationNoticeKey, false);
|
||||
if (!deprecationNoticeShown) {
|
||||
void vscode.window.showWarningMessage(localize('bdc.deprecationWarning', 'The Big Data Cluster add-on is being retired and Azure Data Studio functionality for it will be removed in an upcoming release. Read more about this and support going forward [here](https://go.microsoft.com/fwlink/?linkid=2207340).'));
|
||||
void extensionContext.globalState.update(deprecationNoticeKey, true);
|
||||
}
|
||||
}
|
||||
vscode.window.registerTreeDataProvider('sqlBigDataCluster', treeDataProvider);
|
||||
registerCommands(extensionContext, treeDataProvider);
|
||||
return {
|
||||
getClusterController(url: string, authType: AuthType, username?: string, password?: string): IClusterController {
|
||||
return new ClusterController(url, authType, username, password);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
}
|
||||
|
||||
function registerCommands(context: vscode.ExtensionContext, treeDataProvider: ControllerTreeDataProvider): void {
|
||||
vscode.commands.registerCommand(commands.ConnectControllerCommand, (node?: TreeNode) => {
|
||||
runThrottledAction(commands.ConnectControllerCommand, () => addBdcController(treeDataProvider, node));
|
||||
});
|
||||
|
||||
vscode.commands.registerCommand(commands.CreateControllerCommand, () => {
|
||||
runThrottledAction(commands.CreateControllerCommand, () => vscode.commands.executeCommand('azdata.resource.deploy', 'sql-bdc', ['sql-bdc']));
|
||||
});
|
||||
|
||||
vscode.commands.registerCommand(commands.RemoveControllerCommand, async (node: TreeNode) => {
|
||||
await deleteBdcController(treeDataProvider, node);
|
||||
});
|
||||
|
||||
vscode.commands.registerCommand(commands.RefreshControllerCommand, (node: TreeNode) => {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
treeDataProvider.notifyNodeChanged(node);
|
||||
});
|
||||
|
||||
vscode.commands.registerCommand(commands.ManageControllerCommand, async (info: ControllerNode | BdcDashboardOptions, addOrUpdateController: boolean = false) => {
|
||||
const title: string = `${localize('bdc.dashboard.title', "Big Data Cluster Dashboard -")} ${ControllerNode.toIpAndPort(info.url)}`;
|
||||
if (addOrUpdateController) {
|
||||
// The info may be wrong, but if it is then we'll prompt to reconnect when the dashboard is opened
|
||||
// and update with the correct info then
|
||||
treeDataProvider.addOrUpdateController(
|
||||
info.url,
|
||||
info.auth,
|
||||
info.username,
|
||||
info.password,
|
||||
info.rememberPassword);
|
||||
await treeDataProvider.saveControllers();
|
||||
}
|
||||
const dashboard: BdcDashboard = new BdcDashboard(title, new BdcDashboardModel(info, treeDataProvider));
|
||||
await dashboard.showDashboard();
|
||||
});
|
||||
|
||||
vscode.commands.registerCommand(commands.MountHdfsCommand, e => mountHdfs(e).catch(error => {
|
||||
vscode.window.showErrorMessage(error instanceof Error ? error.message : error);
|
||||
}));
|
||||
vscode.commands.registerCommand(commands.RefreshMountCommand, e => refreshMount(e).catch(error => {
|
||||
vscode.window.showErrorMessage(error instanceof Error ? error.message : error);
|
||||
}));
|
||||
vscode.commands.registerCommand(commands.DeleteMountCommand, e => deleteMount(e).catch(error => {
|
||||
vscode.window.showErrorMessage(error instanceof Error ? error.message : error);
|
||||
}));
|
||||
}
|
||||
|
||||
async function mountHdfs(explorerContext?: azdata.ObjectExplorerContext): Promise<void> {
|
||||
const mountProps = await getMountProps(explorerContext);
|
||||
if (mountProps) {
|
||||
const dialog = new MountHdfsDialog(new MountHdfsModel(mountProps));
|
||||
try {
|
||||
await dialog.showDialog();
|
||||
} catch (error) {
|
||||
if (!(error instanceof HdfsDialogCancelledError)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshMount(explorerContext?: azdata.ObjectExplorerContext): Promise<void> {
|
||||
const mountProps = await getMountProps(explorerContext);
|
||||
if (mountProps) {
|
||||
const dialog = new RefreshMountDialog(new RefreshMountModel(mountProps));
|
||||
await dialog.showDialog();
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteMount(explorerContext?: azdata.ObjectExplorerContext): Promise<void> {
|
||||
const mountProps = await getMountProps(explorerContext);
|
||||
if (mountProps) {
|
||||
const dialog = new DeleteMountDialog(new DeleteMountModel(mountProps));
|
||||
await dialog.showDialog();
|
||||
}
|
||||
}
|
||||
|
||||
async function getMountProps(explorerContext?: azdata.ObjectExplorerContext): Promise<MountHdfsProperties | undefined> {
|
||||
let endpoint = await lookupController(explorerContext);
|
||||
if (!endpoint) {
|
||||
vscode.window.showErrorMessage(endpointNotFoundError);
|
||||
return undefined;
|
||||
}
|
||||
let profile = explorerContext.connectionProfile;
|
||||
let mountProps: MountHdfsProperties = {
|
||||
url: endpoint,
|
||||
auth: profile.authenticationType === azdata.connection.AuthenticationType.SqlLogin ? 'basic' : 'integrated',
|
||||
username: profile.userName,
|
||||
password: profile.password,
|
||||
hdfsPath: getHdsfPath(explorerContext.nodeInfo.nodePath)
|
||||
};
|
||||
return mountProps;
|
||||
}
|
||||
|
||||
function getHdsfPath(nodePath: string): string {
|
||||
const hdfsNodeLabel = '/HDFS';
|
||||
let index = nodePath.indexOf(hdfsNodeLabel);
|
||||
if (index >= 0) {
|
||||
let subPath = nodePath.substring(index + hdfsNodeLabel.length);
|
||||
return subPath.length > 0 ? subPath : '/';
|
||||
}
|
||||
// Use the root
|
||||
return '/';
|
||||
}
|
||||
|
||||
async function lookupController(explorerContext?: azdata.ObjectExplorerContext): Promise<string | undefined> {
|
||||
if (!explorerContext) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let serverInfo = await azdata.connection.getServerInfo(explorerContext.connectionProfile.id);
|
||||
if (!serverInfo || !serverInfo.options) {
|
||||
vscode.window.showErrorMessage(endpointNotFoundError);
|
||||
return undefined;
|
||||
}
|
||||
return getControllerEndpoint(serverInfo);
|
||||
}
|
||||
|
||||
function addBdcController(treeDataProvider: ControllerTreeDataProvider, node?: TreeNode): void {
|
||||
let model = new AddControllerDialogModel(treeDataProvider, node as ControllerNode);
|
||||
let dialog = new AddControllerDialog(model);
|
||||
dialog.showDialog();
|
||||
}
|
||||
|
||||
async function deleteBdcController(treeDataProvider: ControllerTreeDataProvider, node: TreeNode): Promise<boolean | undefined> {
|
||||
if (!node && !(node instanceof ControllerNode)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let controllerNode = node as ControllerNode;
|
||||
|
||||
let choices: { [id: string]: boolean } = {};
|
||||
choices[localize('textYes', "Yes")] = true;
|
||||
choices[localize('textNo', "No")] = false;
|
||||
|
||||
let options = {
|
||||
ignoreFocusOut: false,
|
||||
placeHolder: localize('textConfirmRemoveController', "Are you sure you want to remove \'{0}\'?", controllerNode.label)
|
||||
};
|
||||
|
||||
let result = await vscode.window.showQuickPick(Object.keys(choices), options);
|
||||
let remove: boolean = !!(result && choices[result]);
|
||||
if (remove) {
|
||||
await removeControllerInternal(treeDataProvider, controllerNode);
|
||||
}
|
||||
return remove;
|
||||
}
|
||||
|
||||
async function removeControllerInternal(treeDataProvider: ControllerTreeDataProvider, controllerNode: ControllerNode): Promise<void> {
|
||||
const removed = treeDataProvider.removeController(controllerNode.url, controllerNode.auth, controllerNode.username);
|
||||
if (removed) {
|
||||
await treeDataProvider.saveControllers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throttles actions to avoid bug where on clicking in tree, action gets called twice
|
||||
* instead of once. Any right-click action is safe, just the default on-click action in a tree
|
||||
*/
|
||||
function runThrottledAction(id: string, action: () => void) {
|
||||
let timer = throttleTimers[id];
|
||||
if (!timer) {
|
||||
throttleTimers[id] = timer = setTimeout(() => {
|
||||
action();
|
||||
clearTimeout(timer);
|
||||
throttleTimers[id] = undefined;
|
||||
}, 150);
|
||||
}
|
||||
// else ignore this as we got an identical action in the last 150ms
|
||||
}
|
||||
9
extensions/big-data-cluster/src/typings/refs.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/// <reference path='../../../../src/sql/azdata.d.ts'/>
|
||||
/// <reference path='../../../../src/sql/azdata.proposed.d.ts'/>
|
||||
/// <reference path='../../../../src/vscode-dts/vscode.d.ts'/>
|
||||
/// <reference types='@types/node'/>
|
||||
24
extensions/big-data-cluster/tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compileOnSave": true,
|
||||
"compilerOptions": {
|
||||
"outDir": "./out",
|
||||
"lib": [
|
||||
"es6",
|
||||
"es2015.promise"
|
||||
],
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "node",
|
||||
"declaration": false,
|
||||
"typeRoots": [
|
||||
"./node_modules/@types"
|
||||
],
|
||||
"strict": false,
|
||||
"noUnusedParameters": false,
|
||||
"strictNullChecks": false
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
377
extensions/big-data-cluster/yarn.lock
Normal file
@@ -0,0 +1,377 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@microsoft/ads-kerberos@^1.1.3":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@microsoft/ads-kerberos/-/ads-kerberos-1.1.3.tgz#a10c6d2d0751c0b67548d51a4bc45a7c33d00088"
|
||||
integrity sha512-jlji4IUfkA/4idYBN9tyouCwurfyGZrDzMGlaUmx9/vtwRvdoEFCg959WfhShcfMPVLku/bztmLeZtN1ifccnQ==
|
||||
dependencies:
|
||||
nan "^2.14.0"
|
||||
|
||||
"@types/caseless@*":
|
||||
version "0.12.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8"
|
||||
integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==
|
||||
|
||||
"@types/node@*":
|
||||
version "12.12.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.6.tgz#a47240c10d86a9a57bb0c633f0b2e0aea9ce9253"
|
||||
integrity sha512-FjsYUPzEJdGXjwKqSpE0/9QEh6kzhTAeObA54rn6j3rR4C/mzpI9L0KNfoeASSPMMdxIsoJuCLDWcM/rVjIsSA==
|
||||
|
||||
"@types/request@^2.48.3":
|
||||
version "2.48.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.3.tgz#970b8ed2317568c390361d29c555a95e74bd6135"
|
||||
integrity sha512-3Wo2jNYwqgXcIz/rrq18AdOZUQB8cQ34CXZo+LUwPJNpvRAL86+Kc2wwI8mqpz9Cr1V+enIox5v+WZhy/p3h8w==
|
||||
dependencies:
|
||||
"@types/caseless" "*"
|
||||
"@types/node" "*"
|
||||
"@types/tough-cookie" "*"
|
||||
form-data "^2.5.0"
|
||||
|
||||
"@types/tough-cookie@*":
|
||||
version "2.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.5.tgz#9da44ed75571999b65c37b60c9b2b88db54c585d"
|
||||
integrity sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==
|
||||
|
||||
ajv@^6.5.5:
|
||||
version "6.12.6"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
||||
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
|
||||
dependencies:
|
||||
fast-deep-equal "^3.1.1"
|
||||
fast-json-stable-stringify "^2.0.0"
|
||||
json-schema-traverse "^0.4.1"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
asn1@~0.2.3:
|
||||
version "0.2.4"
|
||||
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
|
||||
integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
|
||||
dependencies:
|
||||
safer-buffer "~2.1.0"
|
||||
|
||||
assert-plus@1.0.0, assert-plus@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
|
||||
integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
|
||||
|
||||
aws-sign2@~0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
|
||||
integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
|
||||
|
||||
aws4@^1.8.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
|
||||
integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
|
||||
|
||||
bcrypt-pbkdf@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
|
||||
integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=
|
||||
dependencies:
|
||||
tweetnacl "^0.14.3"
|
||||
|
||||
caseless@~0.12.0:
|
||||
version "0.12.0"
|
||||
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
|
||||
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
|
||||
|
||||
combined-stream@^1.0.6, combined-stream@~1.0.6:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
core-util-is@1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
||||
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
|
||||
|
||||
dashdash@^1.12.0:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
|
||||
integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=
|
||||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
|
||||
|
||||
ecc-jsbn@~0.1.1:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
|
||||
integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=
|
||||
dependencies:
|
||||
jsbn "~0.1.0"
|
||||
safer-buffer "^2.1.0"
|
||||
|
||||
extend@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
|
||||
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
|
||||
|
||||
extsprintf@1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
|
||||
integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=
|
||||
|
||||
extsprintf@^1.2.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
|
||||
integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8=
|
||||
|
||||
fast-deep-equal@^3.1.1:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||
|
||||
fast-json-stable-stringify@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
|
||||
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
|
||||
|
||||
forever-agent@~0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
|
||||
integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
|
||||
|
||||
form-data@^2.5.0:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4"
|
||||
integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.6"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
form-data@~2.3.2:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
|
||||
integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.6"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
getpass@^0.1.1:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
|
||||
integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=
|
||||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
|
||||
har-schema@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
|
||||
integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
|
||||
|
||||
har-validator@~5.1.0:
|
||||
version "5.1.3"
|
||||
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
|
||||
integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
|
||||
dependencies:
|
||||
ajv "^6.5.5"
|
||||
har-schema "^2.0.0"
|
||||
|
||||
http-signature@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
|
||||
integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=
|
||||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
jsprim "^1.2.2"
|
||||
sshpk "^1.7.0"
|
||||
|
||||
is-typedarray@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
|
||||
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
|
||||
|
||||
isstream@~0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
|
||||
integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
|
||||
|
||||
jsbn@~0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
|
||||
integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
|
||||
|
||||
json-schema-traverse@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
|
||||
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
|
||||
|
||||
json-schema@0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5"
|
||||
integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==
|
||||
|
||||
json-stringify-safe@~5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
|
||||
integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=
|
||||
|
||||
jsprim@^1.2.2:
|
||||
version "1.4.2"
|
||||
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb"
|
||||
integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==
|
||||
dependencies:
|
||||
assert-plus "1.0.0"
|
||||
extsprintf "1.3.0"
|
||||
json-schema "0.4.0"
|
||||
verror "1.10.0"
|
||||
|
||||
mime-db@1.40.0:
|
||||
version "1.40.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32"
|
||||
integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==
|
||||
|
||||
mime-types@^2.1.12, mime-types@~2.1.19:
|
||||
version "2.1.24"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81"
|
||||
integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==
|
||||
dependencies:
|
||||
mime-db "1.40.0"
|
||||
|
||||
nan@^2.14.0:
|
||||
version "2.15.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee"
|
||||
integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==
|
||||
|
||||
oauth-sign@~0.9.0:
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
|
||||
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
|
||||
|
||||
performance-now@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
|
||||
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
|
||||
|
||||
psl@^1.1.24:
|
||||
version "1.1.32"
|
||||
resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.32.tgz#3f132717cf2f9c169724b2b6caf373cf694198db"
|
||||
integrity sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g==
|
||||
|
||||
punycode@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
|
||||
integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
|
||||
|
||||
punycode@^2.1.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
||||
|
||||
qs@~6.5.2:
|
||||
version "6.5.3"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad"
|
||||
integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==
|
||||
|
||||
request@^2.88.0:
|
||||
version "2.88.0"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
|
||||
integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==
|
||||
dependencies:
|
||||
aws-sign2 "~0.7.0"
|
||||
aws4 "^1.8.0"
|
||||
caseless "~0.12.0"
|
||||
combined-stream "~1.0.6"
|
||||
extend "~3.0.2"
|
||||
forever-agent "~0.6.1"
|
||||
form-data "~2.3.2"
|
||||
har-validator "~5.1.0"
|
||||
http-signature "~1.2.0"
|
||||
is-typedarray "~1.0.0"
|
||||
isstream "~0.1.2"
|
||||
json-stringify-safe "~5.0.1"
|
||||
mime-types "~2.1.19"
|
||||
oauth-sign "~0.9.0"
|
||||
performance-now "^2.1.0"
|
||||
qs "~6.5.2"
|
||||
safe-buffer "^5.1.2"
|
||||
tough-cookie "~2.4.3"
|
||||
tunnel-agent "^0.6.0"
|
||||
uuid "^3.3.2"
|
||||
|
||||
safe-buffer@^5.0.1, safe-buffer@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
|
||||
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
|
||||
|
||||
safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
sshpk@^1.7.0:
|
||||
version "1.16.1"
|
||||
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
|
||||
integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==
|
||||
dependencies:
|
||||
asn1 "~0.2.3"
|
||||
assert-plus "^1.0.0"
|
||||
bcrypt-pbkdf "^1.0.0"
|
||||
dashdash "^1.12.0"
|
||||
ecc-jsbn "~0.1.1"
|
||||
getpass "^0.1.1"
|
||||
jsbn "~0.1.0"
|
||||
safer-buffer "^2.0.2"
|
||||
tweetnacl "~0.14.0"
|
||||
|
||||
tough-cookie@~2.4.3:
|
||||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
|
||||
integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==
|
||||
dependencies:
|
||||
psl "^1.1.24"
|
||||
punycode "^1.4.1"
|
||||
|
||||
tunnel-agent@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
|
||||
integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=
|
||||
dependencies:
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
|
||||
version "0.14.5"
|
||||
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
|
||||
integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
|
||||
|
||||
uri-js@^4.2.2:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
|
||||
integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
|
||||
dependencies:
|
||||
punycode "^2.1.0"
|
||||
|
||||
uuid@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
|
||||
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
|
||||
|
||||
verror@1.10.0:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
|
||||
integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=
|
||||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
core-util-is "1.0.2"
|
||||
extsprintf "^1.2.0"
|
||||
|
||||
vscode-nls@^4.0.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c"
|
||||
integrity sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A==
|
||||