Vnet addition

This commit is contained in:
Sourabh Jain 2025-03-22 15:33:56 +05:30
parent 4313d6ecbd
commit 10b0da2190
5 changed files with 318 additions and 79 deletions

View File

@ -42,6 +42,11 @@ export interface DatabaseAccountExtendedProperties {
publicNetworkAccess?: string; publicNetworkAccess?: string;
enablePriorityBasedExecution?: boolean; enablePriorityBasedExecution?: boolean;
vcoreMongoEndpoint?: string; vcoreMongoEndpoint?: string;
virtualNetworkRules?: VNetRule[];
}
export interface VNetRule {
id: string;
} }
export interface DatabaseAccountResponseLocation { export interface DatabaseAccountResponseLocation {

View File

@ -70,7 +70,6 @@ export const commands = (terminalKind: TerminalKind, config?: CommandConfig): st
"psql --version" "psql --version"
]; ];
case TerminalKind.Mongo: case TerminalKind.Mongo:
case TerminalKind.VCoreMongo:
return [ return [
// 1. Fetch and display location details in a readable format // 1. Fetch and display location details in a readable format
"curl -s https://ipinfo.io | jq -r '\"Region: \" + .region + \" Country: \" + .country + \" City: \" + .city + \" IP Addr: \" + .ip'", "curl -s https://ipinfo.io | jq -r '\"Region: \" + .region + \" Country: \" + .country + \" City: \" + .city + \" IP Addr: \" + .ip'",
@ -89,7 +88,28 @@ export const commands = (terminalKind: TerminalKind, config?: CommandConfig): st
// 8. Verify mongosh installation // 8. Verify mongosh installation
"mongosh --version", "mongosh --version",
// 9. Login to MongoDB // 9. Login to MongoDB
`mongosh --host ${config.host} --port 10255 --username ${config.name} --password ${config.password} --ssl --sslAllowInvalidCertificates` `mongosh --host ${config.host} --port 10255 --username ${config.name} --password ${config.password} --tls --tlsAllowInvalidCertificates`
];
case TerminalKind.VCoreMongo:
return [
// 1. Fetch and display location details in a readable format
"curl -s https://ipinfo.io | jq -r '\"Region: \" + .region + \" Country: \" + .country + \" City: \" + .city + \" IP Addr: \" + .ip'",
// 2. Check if mongosh is installed; if not, proceed with installation
"if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi",
// 3. Download mongosh if not installed
"if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-2.3.8-linux-x64.tgz; fi",
// 4. Extract mongosh package if not installed
"if ! command -v mongosh &> /dev/null; then tar -xvzf mongosh-2.3.8-linux-x64.tgz; fi",
// 5. Move mongosh binaries if not installed
"if ! command -v mongosh &> /dev/null; then mkdir -p ~/mongosh && mv mongosh-2.3.8-linux-x64/* ~/mongosh/; fi",
// 6. Add mongosh to PATH if not installed
"if ! command -v mongosh &> /dev/null; then echo 'export PATH=$HOME/mongosh/bin:$PATH' >> ~/.bashrc; fi",
// 7. Source .bashrc to update PATH (even if mongosh was already installed)
"source ~/.bashrc",
// 8. Verify mongosh installation
"mongosh --version",
// 9. Login to MongoDBmongosh mongodb+srv://<credentials>@neesharma-stage-mongo-vcore.mongocluster.cosmos.azure.com/?authMechanism=SCRAM-SHA-256&retrywrites=false&maxIdleTimeMS=120000\u0007
`mongosh "mongodb+srv://nrj:@${config.endpoint}/?authMechanism=SCRAM-SHA-256&retrywrites=false&maxIdleTimeMS=120000" --tls --tlsAllowInvalidCertificates`
]; ];
case TerminalKind.Cassandra: case TerminalKind.Cassandra:
return [ return [

View File

@ -26,6 +26,15 @@ export const getUserRegion = async (subscriptionId: string, resourceGroup: strin
}; };
export async function getARMInfo<T>(path: string, apiVersion: string = "2024-07-01"): Promise<T> {
return await armRequest<T>({
host: configContext.ARM_ENDPOINT,
path: path,
method: "GET",
apiVersion: apiVersion
});
};
export const deleteUserSettings = async (): Promise<void> => { export const deleteUserSettings = async (): Promise<void> => {
await armRequest<void>({ await armRequest<void>({
host: configContext.ARM_ENDPOINT, host: configContext.ARM_ENDPOINT,
@ -43,26 +52,20 @@ export const getUserSettings = async (): Promise<Settings> => {
apiVersion: "2023-02-01-preview" apiVersion: "2023-02-01-preview"
}); });
return { console.log(resp);
location: resp?.properties?.preferredLocation, return resp;
sessionType: resp?.properties?.sessionType,
osType: resp?.properties?.preferredOsType
};
}; };
export const putEphemeralUserSettings = async (userSubscriptionId: string, userRegion: string) => { export const putEphemeralUserSettings = async (userSubscriptionId: string, userRegion: string, vNetSettings?: object) => {
const ephemeralSettings = { const ephemeralSettings = {
properties: { properties: {
preferredOsType: OsType.Linux, preferredOsType: OsType.Linux,
preferredShellType: ShellType.Bash, preferredShellType: ShellType.Bash,
preferredLocation: userRegion, preferredLocation: userRegion,
terminalSettings: {
fontSize: "Medium",
fontStyle: "monospace"
},
networkType: NetworkType.Default, networkType: NetworkType.Default,
sessionType: SessionType.Ephemeral, sessionType: SessionType.Ephemeral,
userSubscription: userSubscriptionId, userSubscription: userSubscriptionId,
vnetSettings: vNetSettings ?? {}
} }
}; };

View File

@ -2,6 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved. * Copyright (c) Microsoft Corporation. All rights reserved.
*/ */
export const enum OsType { export const enum OsType {
Linux = "linux", Linux = "linux",
Windows = "windows" Windows = "windows"
@ -23,11 +24,25 @@ export const enum SessionType {
} }
export type Settings = { export type Settings = {
location: string; properties: UserSettingProperties
sessionType: SessionType;
osType: OsType;
}; };
export type UserSettingProperties = {
networkType: string;
preferredLocation: string;
preferredOsType: OsType;
preferredShellType: ShellType;
userSubscription: string;
sessionType: SessionType;
vnetSettings: VnetSettings;
}
export type VnetSettings = {
networkProfileResourceId: string;
relayNamespaceResourceId: string;
location: string;
}
export type ProvisionConsoleResponse = { export type ProvisionConsoleResponse = {
properties: { properties: {
osType: OsType; osType: OsType;
@ -48,4 +63,86 @@ export type ConnectTerminalResponse = {
tokenUpdated: boolean; tokenUpdated: boolean;
}; };
export type VnetModel = {
name: string;
id: string;
etag: string;
type: string;
location: string;
tags: Record<string, string>;
properties: {
provisioningState: string;
resourceGuid: string;
addressSpace: {
addressPrefixes: string[];
};
encryption: {
enabled: boolean;
enforcement: string;
};
privateEndpointVNetPolicies: string;
subnets: Array<{
name: string;
id: string;
etag: string;
type: string;
properties: {
provisioningState: string;
addressPrefixes?: string[];
addressPrefix?: string;
networkSecurityGroup?: { id: string };
ipConfigurations?: { id: string }[];
ipConfigurationProfiles?: { id: string }[];
privateEndpoints?: { id: string }[];
serviceEndpoints?: Array<{
provisioningState: string;
service: string;
locations: string[];
}>;
delegations?: Array<{
name: string;
id: string;
etag: string;
type: string;
properties: {
provisioningState: string;
serviceName: string;
actions: string[];
};
}>;
purpose?: string;
privateEndpointNetworkPolicies?: string;
privateLinkServiceNetworkPolicies?: string;
};
}>;
virtualNetworkPeerings: any[];
enableDdosProtection: boolean;
};
};
export type RelayNamespace = {
id: string;
name: string;
type: string;
location: string;
tags: Record<string, string>;
properties: {
metricId: string;
serviceBusEndpoint: string;
provisioningState: string;
status: string;
createdAt: string;
updatedAt: string;
};
sku: {
name: string;
tier: string;
};
};
export type RelayNamespaceResponse = {
value: RelayNamespace[];
};

View File

@ -2,13 +2,14 @@
* Copyright (c) Microsoft Corporation. All rights reserved. * Copyright (c) Microsoft Corporation. All rights reserved.
*/ */
import { RelayNamespaceResponse, VnetModel } from "Explorer/Tabs/CloudShellTab/DataModels";
import { listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts"; import { listKeys } from "Utils/arm/generatedClients/cosmos/databaseAccounts";
import { Terminal } from "xterm"; import { Terminal } from "xterm";
import { TerminalKind } from "../../../Contracts/ViewModels"; import { TerminalKind } from "../../../Contracts/ViewModels";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import { AttachAddon } from "./AttachAddOn"; import { AttachAddon } from "./AttachAddOn";
import { getCommands } from "./Commands"; import { getCommands } from "./Commands";
import { authorizeSession, connectTerminal, deleteUserSettings, getNormalizedRegion, getUserSettings, provisionConsole, putEphemeralUserSettings, registerCloudShellProvider, validateUserSettings, verifyCloudShellProviderRegistration } from "./Data"; import { authorizeSession, connectTerminal, getARMInfo, getNormalizedRegion, getUserSettings, provisionConsole, putEphemeralUserSettings, registerCloudShellProvider, verifyCloudShellProviderRegistration } from "./Data";
import { LogError, LogInfo } from "./LogFormatter"; import { LogError, LogInfo } from "./LogFormatter";
export const startCloudShellTerminal = async (terminal: Terminal, shellType: TerminalKind) => { export const startCloudShellTerminal = async (terminal: Terminal, shellType: TerminalKind) => {
@ -24,6 +25,106 @@ export const startCloudShellTerminal = async (terminal: Terminal, shellType: Ter
throw err; throw err;
} }
const settings = await getUserSettings();
let vNetSettings = {};
if(settings?.properties?.vnetSettings && Object.keys(settings?.properties?.vnetSettings).length > 0) {
terminal.writeln(" Network Profile Resource ID: " + settings.properties.vnetSettings.networkProfileResourceId);
terminal.writeln(" Relay Namespace Resource ID: " + settings.properties.vnetSettings.relayNamespaceResourceId);
vNetSettings = {
networkProfileResourceId: settings.properties.vnetSettings.networkProfileResourceId,
relayNamespaceResourceId: settings.properties.vnetSettings.relayNamespaceResourceId,
location: settings.properties.vnetSettings.location
};
}
else
{
terminal.writeln("\x1B[1;31m No VNet settings found. Continuing with default settings\x1B[0m");
}
terminal.writeln("\x1B[1;37m Press '1' to continue with current or default setting.\x1B[0m");
terminal.writeln("\x1B[1;37m Press '2' to configure new VNet to CloudShell.\x1B[0m");
terminal.writeln("\x1B[1;37m Press '3' to Reset CloudShell VNet Settings.\x1B[0m");
terminal.writeln("\x1B[1;37m Note: To learn how to configure VNet for CloudShell, go to this link https://aka.ms/cloudhellvnetsetup \x1B[0m");
terminal.focus();
let isDefaultSetting = false;
const handleKeyPress = terminal.onKey(async ({ key }: { key: string }) => {
if (key === "1") {
terminal.writeln("\x1B[1;32m Pressed 1, Continuing with current/default settings.\x1B[0m");
isDefaultSetting = true;
handleKeyPress.dispose();
}
else if (key === "2") {
isDefaultSetting = false;
handleKeyPress.dispose();
}
else if (key === "3") {
isDefaultSetting = true;
vNetSettings = {};
handleKeyPress.dispose();
}
else {
terminal.writeln("\x1B[1;31m Entered Wrong Input, only 1 or 2 are allowed. Exiting...\x1B[0m");
setTimeout(() => terminal.dispose(), 2000); // Close terminal after 2 sec
handleKeyPress.dispose();
return;
}
if (!isDefaultSetting) {
terminal.writeln("\x1B[1;32m Pressed 2, Enter below details:\x1B[0m");
const subscriptionId = await askQuestion(terminal, "Existing VNet Subscription ID");
const resourceGroup = await askQuestion(terminal, "Existing VNet Resource Group");
const vNetName = await askQuestion(terminal, "Existing VNet Name");
const vNetConfig = await getARMInfo<VnetModel>(`/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Network/virtualNetworks/${vNetName}`);
terminal.writeln("Suggested Network Profiles:");
const ipConfigurationProfiles = vNetConfig.properties.subnets.reduce<{ id: string }[]>(
(acc, subnet) => acc.concat(subnet.properties.ipConfigurationProfiles || []),
[]
);
for (let i = 0; i < ipConfigurationProfiles.length; i++) {
const match = ipConfigurationProfiles[i].id.match(/\/networkProfiles\/([^/]+)/);
const result = match ? `/${match[1]}` : null;
terminal.writeln(result);
}
const networkProfile = await askQuestion(terminal, "Associated Network Profile");
const relays = await getARMInfo<RelayNamespaceResponse>(
`/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Relay/namespaces`,
"2024-01-01");
terminal.writeln("Suggested Network Relays:");
for (let i = 0; i < relays.value.length; i++) {
terminal.writeln(relays.value[i].name);
}
const relayName = await askQuestion(terminal, "Network Relay");
const networkProfileResourceId = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Network/networkProfiles/${networkProfile}`;
const relayResourceId = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Relay/namespaces/${relayName}`;
// const vNetRules = userContext.databaseAccount.properties?.virtualNetworkRules;
// if(vNetRules && vNetRules.length > 0) {
// for (let i = 0; i < vNetRules.length; i++) {
// const vNetName = vNetRules[i].id;
// terminal.writeln(vNetName);
// }
// }
vNetSettings = {
networkProfileResourceId: networkProfileResourceId,
relayNamespaceResourceId: relayResourceId,
location: userContext.databaseAccount.location
};
}
const region = userContext.databaseAccount?.location; const region = userContext.databaseAccount?.location;
terminal.writeln(LogInfo(` Database Account Region identified as '${region}'`)); terminal.writeln(LogInfo(` Database Account Region identified as '${region}'`));
@ -31,12 +132,12 @@ export const startCloudShellTerminal = async (terminal: Terminal, shellType: Ter
const resolvedRegion = getNormalizedRegion(region, defaultCloudShellRegion); const resolvedRegion = getNormalizedRegion(region, defaultCloudShellRegion);
try { try {
var { socketUri, provisionConsoleResponse, targetUri } = await provisionCloudShellSession(resolvedRegion, terminal); var { socketUri, provisionConsoleResponse, targetUri } = await provisionCloudShellSession(resolvedRegion, terminal, vNetSettings);
} }
catch (err) { catch (err) {
terminal.writeln(LogError(err)); terminal.writeln(LogError(err));
terminal.writeln(LogError(`Unable to provision console in request region, Falling back to default region i.e. ${defaultCloudShellRegion}`)); terminal.writeln(LogError(`Unable to provision console in request region, Falling back to default region i.e. ${defaultCloudShellRegion}`));
var { socketUri, provisionConsoleResponse, targetUri } = await provisionCloudShellSession(defaultCloudShellRegion, terminal); var { socketUri, provisionConsoleResponse, targetUri } = await provisionCloudShellSession(defaultCloudShellRegion, terminal, vNetSettings);
} }
if(!socketUri) { if(!socketUri) {
@ -75,8 +176,34 @@ export const startCloudShellTerminal = async (terminal: Terminal, shellType: Ter
terminal.focus(); terminal.focus();
return socket; return socket;
});
} }
const askQuestion = (terminal: any, question: string) => {
return new Promise<string>((resolve) => {
terminal.write(`\x1B[1;34m${question}: \x1B[0m`);
terminal.focus();
let response = "";
const dataListener = terminal.onData((data: string) => {
if (data === "\r") { // Enter key pressed
terminal.writeln(""); // Move to a new line
dataListener.dispose();
return resolve(response.trim());
} else if (data === "\u007F" || data === "\b") { // Handle backspace
if (response.length > 0) {
response = response.slice(0, -1);
terminal.write("\b \b"); // Move cursor back, clear character
}
} else {
response += data;
terminal.write(data); // Display the typed or pasted characters
}
});
});
};
let keepAliveID: NodeJS.Timeout = null; let keepAliveID: NodeJS.Timeout = null;
let pingCount = 0; let pingCount = 0;
@ -168,7 +295,8 @@ const sendStartupCommands = (socket: WebSocket, initCommands: string) => {
const provisionCloudShellSession = async( const provisionCloudShellSession = async(
resolvedRegion: string, resolvedRegion: string,
terminal: Terminal terminal: Terminal,
vNetSettings: object
): Promise<{ socketUri?: string; provisionConsoleResponse?: any; targetUri?: string }> => { ): Promise<{ socketUri?: string; provisionConsoleResponse?: any; targetUri?: string }> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// Show consent message inside the terminal // Show consent message inside the terminal
@ -183,28 +311,14 @@ const provisionCloudShellSession = async(
if (key.toLowerCase() === "y") { if (key.toLowerCase() === "y") {
terminal.writeln("\x1B[1;32mConsent given. Requesting CloudShell. !\x1B[0m"); terminal.writeln("\x1B[1;32mConsent given. Requesting CloudShell. !\x1B[0m");
terminal.writeln(LogInfo('Resetting user settings...'));
await deleteUserSettings();
terminal.writeln(LogInfo('Applying fresh user settings...')); terminal.writeln(LogInfo('Applying fresh user settings...'));
try { try {
await putEphemeralUserSettings(userContext.subscriptionId, resolvedRegion); await putEphemeralUserSettings(userContext.subscriptionId, resolvedRegion, vNetSettings);
} catch (err) { } catch (err) {
terminal.writeln(LogError('Unable to update user settings to ephemeral session.')); terminal.writeln(LogError('Unable to update user settings to ephemeral session.'));
return reject(err); return reject(err);
} }
// verify user settings after they have been updated to ephemeral
try {
const userSettings = await getUserSettings();
const isValidUserSettings = validateUserSettings(userSettings);
if (!isValidUserSettings) {
throw new Error("Invalid user settings detected for ephemeral session.");
}
} catch (err) {
terminal.writeln(LogError('Unable to verify user settings for ephemeral session.'));
return reject(err);
}
// trigger callback to provision console internal // trigger callback to provision console internal
let provisionConsoleResponse; let provisionConsoleResponse;
try { try {