+
{Object.entries(message.hostStatusCount).map(([status, count]) => (
-
- {`${status}: `}
+ {status.toLowerCase().includes('ssh client error') ? (
+
+
+
+
+ {`${status.toLowerCase()}: `}
+
+
+ {count}
+
+
+
+
+
+
+ {Object.entries(message.sshClientErrorCount).map(
+ ([message, messageCount]) => (
+
+ {`${message}: `}
+
+ {messageCount}
+
+
+ )
+ )}
+
+
+
+ ) : (
- {count}
+ {`${status}: `}
+
+ {count}
+
-
+ )}
))}
@@ -108,6 +195,7 @@ export const CustomProgressBar = ({ message, size, max, value, isStart }) => {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
+ alignItems: 'center',
gap: '5px',
}}
>
diff --git a/jccm/src/Frontend/Layout/InventorySearch/InventorySearch.js b/jccm/src/Frontend/Layout/InventorySearch/InventorySearch.js
index e19bc48..76d9fd7 100644
--- a/jccm/src/Frontend/Layout/InventorySearch/InventorySearch.js
+++ b/jccm/src/Frontend/Layout/InventorySearch/InventorySearch.js
@@ -73,10 +73,10 @@ const InventorySearchCard = ({ isOpen, onClose }) => {
const [facts, setFacts] = useState([]);
const factsColumns = [
- { label: 'Address', name: 'address', width: 8 },
+ { label: 'Address', name: 'address', width: 12 },
{ label: 'Port', name: 'port', width: 5 },
- { label: 'Username', name: 'username', width: 10 },
- { label: 'Password', name: 'password', width: 10 },
+ { label: 'Username', name: 'username', width: 8 },
+ { label: 'Password', name: 'password', width: 8 },
{ label: 'Host Name', name: 'hostName', width: 10 },
{ label: 'Hardware Model', name: 'hardwareModel', width: 10 },
{ label: 'Serial Number', name: 'serialNumber', width: 10 },
diff --git a/jccm/src/Frontend/Layout/InventorySearch/InventorySearchControl.js b/jccm/src/Frontend/Layout/InventorySearch/InventorySearchControl.js
index 0ed2dad..a7841c7 100644
--- a/jccm/src/Frontend/Layout/InventorySearch/InventorySearchControl.js
+++ b/jccm/src/Frontend/Layout/InventorySearch/InventorySearchControl.js
@@ -29,6 +29,8 @@ import {
TriangleLeftRegular,
bundleIcon,
} from '@fluentui/react-icons';
+
+import useStore from '../../Common/StateStore';
import { RotatingIcon } from '../ChangeIcon';
import { CustomProgressBar } from './CustomProgressBar';
import { getHostListMultiple, getHostCountMultiple } from './InventorySearchUtils';
@@ -41,12 +43,13 @@ import { getDeviceFacts } from '../Devices';
export const InventorySearchControl = ({ subnets, startCallback, endCallback, onAddFact }) => {
const { notify } = useNotify(); // Correctly use the hook here
+ const { settings } = useStore();
const [isStart, setIsStart] = useState(false);
const [searchRate, setSearchRate] = useState(10);
const [hostSeq, setHostSeq] = useState(0);
const [hostStatusCount, setHostStatusCount] = useState({});
-
+ const [sshClientErrorCount, setSshClientErrorCount] = useState({});
const isStartRef = useRef(null);
const hostSeqRef = useRef(null);
const hostStatusCountRef = useRef(null);
@@ -62,7 +65,7 @@ export const InventorySearchControl = ({ subnets, startCallback, endCallback, on
hostStatusCountRef.current = hostStatusCount;
}, [isStart, hostSeq, hostStatusCount]);
- const updateHostStatusCount = (status) => {
+ const updateHostStatusCount2 = (status) => {
setHostStatusCount((prevStatus) => {
const updatedStatus = { ...prevStatus };
if (updatedStatus[status]) {
@@ -74,18 +77,48 @@ export const InventorySearchControl = ({ subnets, startCallback, endCallback, on
});
};
+ const updateCount = (result) => {
+ const { status, message } = result;
+
+ setHostStatusCount((prevStatus) => {
+ const updatedStatus = { ...prevStatus };
+ if (updatedStatus[status]) {
+ updatedStatus[status] += 1;
+ } else {
+ updatedStatus[status] = 1;
+ }
+ return updatedStatus;
+ });
+
+ if (status === 'SSH Client Error') {
+ setSshClientErrorCount((prevMessage) => {
+ const updatedMessage = { ...prevMessage };
+ if (updatedMessage[message]) {
+ updatedMessage[message] += 1;
+ } else {
+ updatedMessage[message] = 1;
+ }
+ return updatedMessage;
+ });
+ }
+ };
+
const fetchDeviceFacts = async (device) => {
const maxRetries = 2;
const retryInterval = 1000; // 1 seconds in milliseconds
let response;
+ const bastionHost = settings?.bastionHost || {};
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
- response = await getDeviceFacts({ ...device, timeout: 3000 });
+ response = await getDeviceFacts({ ...device, timeout: 5000 }, false, bastionHost);
+
// console.log(`${device.address}: response: `, response);
if (response.status) {
- updateHostStatusCount(response.result.status);
-
+ // updateHostStatusCount(response.result.status);
+ updateCount(response.result);
+
const { address, port, username, password } = device;
if (!!response.result.vc) {
@@ -136,7 +169,8 @@ export const InventorySearchControl = ({ subnets, startCallback, endCallback, on
}
}
- updateHostStatusCount(response.result.status);
+ // updateHostStatusCount(response.result.status);
+ updateCount(response.result);
return response;
};
@@ -151,6 +185,9 @@ export const InventorySearchControl = ({ subnets, startCallback, endCallback, on
const startTime = Date.now();
const interval = 1000 / searchRate; // Desired interval between each command
+ setHostStatusCount({});
+ setSshClientErrorCount({});
+
for (const device of getHostListMultiple(subnets)) {
promises.push(fetchDeviceFacts(device)); // Add the promise to the array
setHostSeq(n++);
@@ -288,7 +325,7 @@ export const InventorySearchControl = ({ subnets, startCallback, endCallback, on
}}
>
{
const { notify } = useNotify(); // Correctly use the hook here
const fileInputRef = useRef(null);
+ const { settings } = useStore();
+ const [isBastionHostEmpty, setIsBastionHostEmpty] = useState(false);
// Calculate the total sum of hostCounts
const totalHostCount = getHostCountMultiple(items);
@@ -156,6 +159,12 @@ export const SubnetResult = ({ columns, items, onDeleteSubnet, onImportSubnet =
writeFile(wb, fileName);
};
+ useEffect(() => {
+ const bastionHost = settings?.bastionHost || {};
+ const isEmpty = Object.keys(bastionHost).length === 0;
+ setIsBastionHostEmpty(isEmpty);
+ }, [settings]);
+
return (
-
- {/*
-
-
*/}
+
+ {!isBastionHostEmpty && (
+
+
+
+ )}
Total Subnets: {items?.length}
Total Hosts: {totalHostCount.toLocaleString()}
diff --git a/jccm/src/Frontend/Layout/InventoryTreeMenuCloud.js b/jccm/src/Frontend/Layout/InventoryTreeMenuCloud.js
index 289f471..b9aaacd 100644
--- a/jccm/src/Frontend/Layout/InventoryTreeMenuCloud.js
+++ b/jccm/src/Frontend/Layout/InventoryTreeMenuCloud.js
@@ -351,7 +351,7 @@ const RenderCloudInventoryTree = ({ nodes, openItems, onOpenChange }) => {
const onReleaseConfirmButton = async (orgId, mac) => {
setIsOpenReleaseDialog(false);
- console.log('>>>> mac address:', mac);
+ // console.log('>>>> mac address:', mac);
const data = await electronAPI.saProxyCall({
api: `orgs/${orgId}/inventory`,
diff --git a/jccm/src/Frontend/Layout/InventoryTreeMenuLocal.js b/jccm/src/Frontend/Layout/InventoryTreeMenuLocal.js
index 93ac566..089d9cf 100644
--- a/jccm/src/Frontend/Layout/InventoryTreeMenuLocal.js
+++ b/jccm/src/Frontend/Layout/InventoryTreeMenuLocal.js
@@ -800,7 +800,7 @@ const InventoryTreeMenuLocal = () => {
font='numeric'
style={{ color: 'red' }}
>
- {isChecking.error} - Retry attempt {isChecking.retry}
+ {isChecking.error} - Retry {isChecking.retry}
)}
@@ -830,7 +830,7 @@ const InventoryTreeMenuLocal = () => {
font='numeric'
style={{ color: 'red' }}
>
- {isAdopting[path]?.error} - Retry attempt {isAdopting[path]?.retry}
+ {isAdopting[path]?.error} - Retry {isAdopting[path]?.retry}
)}
@@ -861,15 +861,17 @@ const InventoryTreeMenuLocal = () => {
};
const fetchDeviceFacts = async (device) => {
- const maxRetries = 3;
+ const maxRetries = 2;
const retryInterval = 10000; // 10 seconds in milliseconds
let response;
setIsChecking(device._path, { status: true, retry: 0 });
resetIsAdopting(device._path);
+ const bastionHost = settings?.bastionHost || {};
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
- response = await getDeviceFacts({ ...device, timeout: 5000 }, true);
+ response = await getDeviceFacts({ ...device, timeout: 5000 }, true, bastionHost);
if (response.status) {
setDeviceFacts(device._path, response.result);
resetIsChecking(device._path);
@@ -879,11 +881,16 @@ const InventoryTreeMenuLocal = () => {
`${device.address}:${device.port} - Error retrieving facts on attempt ${attempt}:`,
response
);
- if (response.result?.status.toLowerCase().includes('authentication failed')) {
+ if (response.result?.status?.toLowerCase().includes('authentication failed')) {
+ deleteDeviceFacts(device._path);
+ setIsChecking(device._path, { status: false, retry: -1, error: response.result?.message });
+ return;
+ } else if (response.result?.status?.toLowerCase().includes('ssh client error')) {
deleteDeviceFacts(device._path);
setIsChecking(device._path, { status: false, retry: -1, error: response.result?.message });
return;
}
+
setIsChecking(device._path, { status: true, retry: attempt, error: response.result?.message });
await new Promise((resolve) => setTimeout(resolve, retryInterval));
}
@@ -946,15 +953,19 @@ const InventoryTreeMenuLocal = () => {
};
const actionAdoptDevice = async (device, jsiTerm = false, deleteOutboundSSHTerm = false) => {
- const maxRetries = 6;
- const retryInterval = 15 * 1000; // 15 seconds in milliseconds
+ const maxRetries = 2;
+ const retryInterval = 15000;
+ let response;
setIsAdopting(device._path, { status: true, retry: 0 });
resetIsChecking(device._path);
+ const bastionHost = settings?.bastionHost || {};
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
- const response = await adoptDevices(device, jsiTerm, deleteOutboundSSHTerm);
- if (response.status) {
+ response = await adoptDevices(device, jsiTerm, deleteOutboundSSHTerm, bastionHost);
+
+ if (response?.status) {
resetIsAdopting(device.path, false);
return;
} else {
@@ -963,24 +974,26 @@ const InventoryTreeMenuLocal = () => {
response
);
- if (response.result?.status.toLowerCase().includes('authentication failed')) {
- setIsAdopting(device._path, { status: false, retry: -1, error: response.result?.message });
+ if (response?.result?.status?.toLowerCase().includes('authentication failed')) {
+ setIsAdopting(device._path, { status: false, retry: -1, error: response?.result?.message });
return;
}
- setIsAdopting(device._path, { status: true, retry: attempt, error: response.result?.message });
+ setIsAdopting(device._path, { status: true, retry: attempt, error: response?.result?.message });
await new Promise((resolve) => setTimeout(resolve, retryInterval)); // Wait before retrying
}
}
- resetIsAdopting(device._path, { status: false, retry: -1, error: response.result?.message });
+ resetIsAdopting(device._path, { status: false, retry: -1, error: response?.result?.message });
notify(
Device Adoption Failure
-
- The device could not be adopted into the organization: "{device.organization}".
- Error Message: {response.result.message}
+
+
+ The device could not be adopted into the organization: "{device.organization}".
+ Error Message: {response?.result.message}
+
,
{ intent: 'error' }
diff --git a/jccm/src/Services/ApiServer.js b/jccm/src/Services/ApiServer.js
index 04b0026..d1c3f64 100644
--- a/jccm/src/Services/ApiServer.js
+++ b/jccm/src/Services/ApiServer.js
@@ -131,7 +131,9 @@ const serverGetCloudInventory = async (targetOrgs = null) => {
// console.log(`Get device stats: ${JSON.stringify(cloudDeviceStates, null, 2)}`);
for (const cloudDevice of cloudDeviceStates) {
- SN2VSN[cloudDevice.serial] = cloudDevice.module_stat.find(item => item && item.serial);
+ SN2VSN[cloudDevice.serial] = cloudDevice.module_stat.find(
+ (item) => item && item.serial
+ );
}
}
}
@@ -167,6 +169,266 @@ const serverGetCloudInventory = async (targetOrgs = null) => {
return { inventory, isFilterApplied };
};
+const startSSHConnectionStandalone = (event, device, { id, cols, rows }) => {
+ const { address, port, username, password } = device;
+
+ const conn = new Client();
+ sshSessions[id] = conn;
+
+ conn.on('ready', () => {
+ console.log(`SSH session successfully opened for id: ${id}`);
+ event.reply('sshSessionOpened', { id }); // Notify renderer that the session is open
+
+ conn.shell({ cols, rows }, (err, stream) => {
+ if (err) {
+ event.reply('sshErrorOccurred', { id, message: err.message });
+ return;
+ }
+
+ stream.on('data', (data) => {
+ event.reply('sshDataReceived', { id, data: data.toString() });
+ });
+
+ stream.on('close', () => {
+ event.reply('sshDataReceived', { id, data: 'The SSH session has been closed.\r\n' });
+
+ conn.end();
+ delete sshSessions[id];
+ event.reply('sshSessionClosed', { id });
+ // Clean up listeners
+ ipcMain.removeAllListeners(`sendSSHInput-${id}`);
+ ipcMain.removeAllListeners(`resizeSSHSession-${id}`);
+ });
+
+ ipcMain.on(`sendSSHInput-${id}`, (_, data) => {
+ stream.write(data);
+ });
+
+ ipcMain.on(`resizeSSHSession-${id}`, (_, { cols, rows }) => {
+ stream.setWindow(rows, cols, 0, 0);
+ });
+ });
+ }).connect({
+ host: address,
+ port,
+ username,
+ password,
+ poll: 10, // Adjust the polling interval to 10 milliseconds
+ keepaliveInterval: 10000, // Send keepalive every 10 seconds
+ keepaliveCountMax: 3, // Close the connection after 3 failed keepalives
+ });
+
+ conn.on('error', (err) => {
+ event.reply('sshErrorOccurred', { id, message: err.message });
+ });
+
+ conn.on('end', () => {
+ delete sshSessions[id];
+ });
+};
+
+const startSSHConnectionProxy = (event, device, bastionHost, { id, cols, rows }) => {
+ const conn = new Client();
+ sshSessions[id] = conn;
+
+ conn.on('ready', () => {
+ console.log(`SSH session successfully opened for id: ${id}`);
+ event.reply('sshSessionOpened', { id }); // Notify renderer that the session is open
+
+ conn.shell({ cols, rows }, (err, stream) => {
+ if (err) {
+ event.reply('sshErrorOccurred', { id, message: err.message });
+ return;
+ }
+
+ let initialOutputBuffer = '';
+ let sshClientCommand = '';
+
+ const sshOptions = [
+ '-o StrictHostKeyChecking=no',
+ '-o ConnectTimeout=3',
+ '-o NumberOfPasswordPrompts=1',
+ '-o PreferredAuthentications=keyboard-interactive',
+ ].join(' ');
+
+ const linuxSSHCommand = `ssh -tt ${sshOptions} -p ${device.port} ${device.username}@${device.address};exit`;
+ const junosSSHCommand = `set cli prompt "> "\nstart shell command "ssh -tt ${sshOptions} -p ${device.port} ${device.username}@${device.address}"\nexit`;
+
+ const promptPattern = /\n[\s\S]*?[@#>%$]\s$/;
+
+ let isBastionHostOsTypeChecked = false;
+ let isBastionHostOsTypeCheckTimeoutPass = false;
+ let bastionHostOsTypeCheckTimeoutHandle;
+
+ let isSshClientPasswordInputted = false;
+ let isSshClientPasswordInputTimeoutPass = false;
+ let sshClientPasswordInputTimeoutHandle;
+
+ let isSshClientAuthErrorMonitoringTimeoutPass = false;
+ let sshClientAuthErrorMonitoringTimeoutHandle;
+
+ const resetBastionHostOsTypeCheckTimeout = (timeout) => {
+ clearTimeout(bastionHostOsTypeCheckTimeoutHandle);
+ bastionHostOsTypeCheckTimeoutHandle = setTimeout(() => {
+ isBastionHostOsTypeCheckTimeoutPass = true;
+ }, timeout);
+ };
+
+ const resetSshClientPasswordInputTimeout = (timeout) => {
+ clearTimeout(sshClientPasswordInputTimeoutHandle);
+ sshClientPasswordInputTimeoutHandle = setTimeout(() => {
+ isSshClientPasswordInputTimeoutPass = true;
+ }, timeout);
+ };
+
+ const resetSshClientAuthErrorMonitoringTimeout = (timeout) => {
+ clearTimeout(sshClientAuthErrorMonitoringTimeoutHandle);
+ sshClientAuthErrorMonitoringTimeoutHandle = setTimeout(() => {
+ isSshClientAuthErrorMonitoringTimeoutPass = true;
+ }, timeout);
+ };
+
+ resetBastionHostOsTypeCheckTimeout(5000);
+
+ stream.on('data', (data) => {
+ const output = data.toString();
+
+ // process.stdout.write(output);
+
+ if (!isBastionHostOsTypeCheckTimeoutPass || !isSshClientPasswordInputTimeoutPass) {
+ initialOutputBuffer += output;
+ }
+
+ // Check if either the timeout has passed without a check, or the check hasn't been done and it's ready
+ if (
+ (!isBastionHostOsTypeChecked && promptPattern.test(initialOutputBuffer)) ||
+ (isBastionHostOsTypeCheckTimeoutPass && !isBastionHostOsTypeChecked)
+ ) {
+ isBastionHostOsTypeChecked = true;
+ isBastionHostOsTypeCheckTimeoutPass = true;
+ clearTimeout(bastionHostOsTypeCheckTimeoutHandle);
+
+ // Determine the SSH command based on the output buffer content
+ if (initialOutputBuffer.toLowerCase().includes('junos') && bastionHost.username !== 'root') {
+ sshClientCommand = junosSSHCommand;
+ } else {
+ sshClientCommand = linuxSSHCommand;
+ }
+
+ // Send the determined SSH command and reset the initial output buffer
+ stream.write(sshClientCommand + '\n');
+ initialOutputBuffer = '';
+
+ // Reset the password input timeout to prepare for the next step
+ resetSshClientPasswordInputTimeout(5000);
+ }
+
+ // Check if the bastion host's OS type has been successfully checked.
+ if (isBastionHostOsTypeChecked) {
+ // Handling input for the SSH client password
+ if (!isSshClientPasswordInputted) {
+ if (
+ !isSshClientPasswordInputTimeoutPass &&
+ initialOutputBuffer.toLowerCase().includes('password:')
+ ) {
+ // Password prompt found, input the password
+ isSshClientPasswordInputted = true;
+ stream.write(device.password + '\n');
+ initialOutputBuffer = '';
+ resetSshClientAuthErrorMonitoringTimeout(5000);
+ } else if (isSshClientPasswordInputTimeoutPass) {
+ // Handling timeout passing without password input
+ isSshClientPasswordInputted = true;
+ initialOutputBuffer = '';
+ isSshClientAuthErrorMonitoringTimeoutPass = true;
+ }
+ } else {
+ // Handling SSH client authentication error monitoring
+ if (!isSshClientAuthErrorMonitoringTimeoutPass) {
+ // Check if there's no error message indicating a connection issue
+ const connectionErrorMessage = `${device.username}@${device.address}: `;
+ const sshConnectionErrorMessage = `ssh: connect to host ${device.address} port ${device.port}: `;
+ if (
+ !initialOutputBuffer.includes(connectionErrorMessage) &&
+ !initialOutputBuffer.includes(sshConnectionErrorMessage)
+ ) {
+ event.reply('sshDataReceived', { id, data: output });
+ }
+ } else {
+ // Timeout has passed, send data regardless
+ event.reply('sshDataReceived', { id, data: output });
+ }
+ }
+ }
+ });
+
+ stream.on('close', () => {
+ // console.log(`|||>>>${initialOutputBuffer}<<<|||`);
+
+ // Function to handle the extraction and cleanup of error messages
+ function handleSSHErrorMessage(pattern, messagePrefix) {
+ const regex = new RegExp(`^${pattern}(.+)$`, 'm');
+ const match = initialOutputBuffer.match(regex);
+
+ if (match && match[1]) {
+ const sshErrorMessage = match[1];
+ const cleanedErrorMessage = sshErrorMessage.replace(/\.$/, '');
+ event.reply('sshDataReceived', {
+ id,
+ data: `${messagePrefix}${cleanedErrorMessage}\r\n\r\n`,
+ });
+ }
+ }
+
+ // Check for error messages related to direct SSH or port issues
+ if (initialOutputBuffer.includes(`${device.username}@${device.address}: `)) {
+ handleSSHErrorMessage(`${device.username}@${device.address}: `, 'SSH connection: ');
+ } else if (
+ initialOutputBuffer.includes(`ssh: connect to host ${device.address} port ${device.port}: `)
+ ) {
+ handleSSHErrorMessage(
+ `ssh: connect to host ${device.address} port ${device.port}: `,
+ '\r\nSSH connection: '
+ );
+ }
+
+ conn.end();
+ delete sshSessions[id];
+ event.reply('sshSessionClosed', { id });
+
+ // Clean up listeners
+ ipcMain.removeAllListeners(`sendSSHInput-${id}`);
+ ipcMain.removeAllListeners(`resizeSSHSession-${id}`);
+ });
+
+ ipcMain.on(`sendSSHInput-${id}`, (_, data) => {
+ stream.write(data);
+ });
+
+ ipcMain.on(`resizeSSHSession-${id}`, (_, { cols, rows }) => {
+ stream.setWindow(rows, cols, 0, 0);
+ });
+ });
+ }).connect({
+ host: bastionHost.host,
+ port: bastionHost.port,
+ username: bastionHost.username,
+ password: bastionHost.password,
+ readyTimeout: bastionHost.readyTimeout,
+ poll: 10, // Adjust the polling interval to 10 milliseconds
+ keepaliveInterval: 10000, // Send keepalive every 10 seconds
+ keepaliveCountMax: 3, // Close the connection after 3 failed keepalives
+ });
+
+ conn.on('error', (err) => {
+ event.reply('sshErrorOccurred', { id, message: err.message });
+ });
+
+ conn.on('end', () => {
+ delete sshSessions[id];
+ });
+};
+
export const setupApiHandlers = () => {
ipcMain.handle('saFetchAvailableClouds', async (event) => {
console.log('main: saFetchAvailableClouds');
@@ -360,6 +622,8 @@ export const setupApiHandlers = () => {
ipcMain.on('startSSHConnection', async (event, { id, cols, rows }) => {
console.log('main: startSSHConnection: id: ' + id);
const inventory = await msGetLocalInventory();
+ const settings = await msLoadSettings();
+ const bastionHost = settings?.bastionHost || {};
const found = inventory.filter(
({ organization, site, address, port }) => id === `/Inventory/${organization}/${site}/${address}/${port}`
@@ -369,61 +633,11 @@ export const setupApiHandlers = () => {
return;
}
const device = found[0];
- const { address, port, username, password } = device;
-
- const conn = new Client();
- sshSessions[id] = conn;
-
- conn.on('ready', () => {
- console.log(`SSH session successfully opened for id: ${id}`);
- event.reply('sshSessionOpened', { id }); // Notify renderer that the session is open
-
- conn.shell({ cols, rows }, (err, stream) => {
- if (err) {
- event.reply('sshErrorOccurred', { id, message: err.message });
- return;
- }
-
- stream.on('data', (data) => {
- event.reply('sshDataReceived', { id, data: data.toString() });
- });
-
- stream.on('close', () => {
- event.reply('sshDataReceived', { id, data: 'The SSH session has been closed.\r\n' });
-
- conn.end();
- delete sshSessions[id];
- event.reply('sshSessionClosed', { id });
- // Clean up listeners
- ipcMain.removeAllListeners(`sendSSHInput-${id}`);
- ipcMain.removeAllListeners(`resizeSSHSession-${id}`);
- });
-
- ipcMain.on(`sendSSHInput-${id}`, (_, data) => {
- stream.write(data);
- });
-
- ipcMain.on(`resizeSSHSession-${id}`, (_, { cols, rows }) => {
- stream.setWindow(rows, cols, 0, 0);
- });
- });
- }).connect({
- host: address,
- port,
- username,
- password,
- poll: 10, // Adjust the polling interval to 10 milliseconds
- keepaliveInterval: 10000, // Send keepalive every 10 seconds
- keepaliveCountMax: 3, // Close the connection after 3 failed keepalives
- });
-
- conn.on('error', (err) => {
- event.reply('sshErrorOccurred', { id, message: err.message });
- });
-
- conn.on('end', () => {
- delete sshSessions[id];
- });
+ if (bastionHost?.active) {
+ startSSHConnectionProxy(event, device, bastionHost, { id, cols, rows });
+ } else {
+ startSSHConnectionStandalone(event, device, { id, cols, rows });
+ }
});
ipcMain.on('disconnectSSHSession', (event, { id }) => {
@@ -439,8 +653,16 @@ export const setupApiHandlers = () => {
console.log('main: saGetDeviceFacts');
try {
- const { address, port, username, password, timeout, upperSerialNumber } = args;
- const reply = await getDeviceFacts(address, port, username, password, timeout, upperSerialNumber);
+ const { address, port, username, password, timeout, upperSerialNumber, bastionHost } = args;
+ const reply = await getDeviceFacts(
+ address,
+ port,
+ username,
+ password,
+ timeout,
+ upperSerialNumber,
+ bastionHost
+ );
return { facts: true, reply };
} catch (error) {
@@ -451,8 +673,18 @@ export const setupApiHandlers = () => {
ipcMain.handle('saAdoptDevice', async (event, args) => {
console.log('main: saAdoptDevice');
- const { organization, site, address, port, username, password, jsiTerm, deleteOutboundSSHTerm, ...others } =
- args;
+ const {
+ organization,
+ site,
+ address,
+ port,
+ username,
+ password,
+ jsiTerm,
+ deleteOutboundSSHTerm,
+ bastionHost,
+ ...others
+ } = args;
const cloudOrgs = await msGetCloudOrgs();
const orgId = cloudOrgs[organization]?.id;
@@ -471,7 +703,7 @@ export const setupApiHandlers = () => {
? `delete system services outbound-ssh\n${response.cmd}\n`
: `${response.cmd}\n`;
- const reply = await commitJunosSetConfig(address, port, username, password, configCommand);
+ const reply = await commitJunosSetConfig(address, port, username, password, configCommand, bastionHost);
if (reply.status === 'success' && reply.data.includes('')) {
return { adopt: true, reply };
diff --git a/jccm/src/Services/Device.js b/jccm/src/Services/Device.js
index 68b779e..9365377 100644
--- a/jccm/src/Services/Device.js
+++ b/jccm/src/Services/Device.js
@@ -30,6 +30,10 @@ const StatusErrorMessages = {
status: 'Inactivity timeout',
message: 'Session closed due to inactivity',
},
+ SSH_CLIENT_ERROR: {
+ status: 'SSH Client Error',
+ message: '',
+ },
};
/**
@@ -40,7 +44,44 @@ const StatusErrorMessages = {
* @param {number} commitInactivityTimeout - Timeout in milliseconds for inactivity on commit commands.
* @returns {Promise