diff --git a/jccm/package.json b/jccm/package.json
index 6a14312..95f033b 100644
--- a/jccm/package.json
+++ b/jccm/package.json
@@ -1,7 +1,7 @@
{
"name": "jccm",
"productName": "Juniper Cloud Connection Manager",
- "version": "1.1.0",
+ "version": "1.1.1",
"description": "Juniper Cloud Connection Manager",
"main": ".webpack/main",
"scripts": {
diff --git a/jccm/src/Frontend/Common/StateStore.js b/jccm/src/Frontend/Common/StateStore.js
index 4bc248f..a640024 100644
--- a/jccm/src/Frontend/Common/StateStore.js
+++ b/jccm/src/Frontend/Common/StateStore.js
@@ -67,9 +67,9 @@ const useStore = create((set, get) => ({
const orgs = {};
user?.privileges.forEach((item) => {
if (item.scope === 'org') {
- const orgId = item.org_id
+ const orgId = item.org_id;
const orgName = item.name;
- orgs[orgId] = orgName;
+ orgs[orgId] = orgName;
}
});
return { user, orgs };
@@ -332,6 +332,24 @@ const useStore = create((set, get) => ({
};
}),
+ cleanUpDeviceFacts: async () => {
+ const state = get();
+ const inventoryPaths = new Set(state.inventory.map((item) => item._path));
+ const cleanedDeviceFacts = Object.fromEntries(
+ Object.entries(state.deviceFacts).filter(([key]) => inventoryPaths.has(key))
+ );
+
+ console.log('inventoryPaths', inventoryPaths);
+ console.log('state.deviceFacts', state.deviceFacts);
+ console.log('cleanedDeviceFacts', cleanedDeviceFacts);
+
+ await electronAPI.saSaveDeviceFacts({ facts: cleanedDeviceFacts });
+
+ set(() => ({
+ deviceFacts: cleanedDeviceFacts,
+ }));
+ },
+
deleteDeviceFacts: (path) =>
set((state) => {
const { [path]: _, ...rest } = state.deviceFacts;
diff --git a/jccm/src/Frontend/Layout/ChangeIcon.js b/jccm/src/Frontend/Layout/ChangeIcon.js
index 4f1df0b..4ecc4c6 100644
--- a/jccm/src/Frontend/Layout/ChangeIcon.js
+++ b/jccm/src/Frontend/Layout/ChangeIcon.js
@@ -37,7 +37,7 @@ export const CircleIcon = ({ Icon, color = tokens.colorPaletteGreenBorder2, size
width: `calc(${size} + 2px)`,
height: `calc(${size} + 2px)`,
borderRadius: '50%',
- border: `2px solid ${color}`,
+ border: `0.5px solid ${color}`,
}}
>
diff --git a/jccm/src/Frontend/Layout/Devices.js b/jccm/src/Frontend/Layout/Devices.js
index 8a78172..6c96208 100644
--- a/jccm/src/Frontend/Layout/Devices.js
+++ b/jccm/src/Frontend/Layout/Devices.js
@@ -1,8 +1,8 @@
const { electronAPI } = window;
-export const adoptDevices = async (device, jsiTerm=false) => {
+export const adoptDevices = async (device, jsiTerm=false, deleteOutboundSSHTerm=false) => {
const { address, port, username, password, organization, site } = device;
- const response = await electronAPI.saAdoptDevice({ address, port, username, password, organization, site, jsiTerm });
+ const response = await electronAPI.saAdoptDevice({ address, port, username, password, organization, site, jsiTerm, deleteOutboundSSHTerm });
if (response.adopt) {
return { status: true, result: response.result };
@@ -12,8 +12,8 @@ export const adoptDevices = async (device, jsiTerm=false) => {
}
};
-export const releaseDevices = async (device, serialNumber) => {
- const { organization } = device;
+export const releaseDevices = async (deviceInfo) => {
+ const { organization, serialNumber} = deviceInfo;
const response = await electronAPI.saReleaseDevice({ organization, serial: serialNumber });
if (response.release) {
diff --git a/jccm/src/Frontend/Layout/Footer.js b/jccm/src/Frontend/Layout/Footer.js
index ca2593a..d2170fd 100644
--- a/jccm/src/Frontend/Layout/Footer.js
+++ b/jccm/src/Frontend/Layout/Footer.js
@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react';
+import React, { useEffect, useState } from 'react';
import { Label, Button, tokens } from '@fluentui/react-components';
import { HexagonThreeRegular, HexagonThreeFilled, bundleIcon } from '@fluentui/react-icons';
@@ -7,12 +7,49 @@ import useStore from '../Common/StateStore';
import { BastionHostButton } from './BastionHostButton';
export default () => {
- const { inventory, deviceFacts, cloudDevices } = useStore();
+ const { isUserLoggedIn, inventory, deviceFacts, cloudDevices, cloudInventory } = useStore();
+ const [countOfOrgOrSiteUnmatched, setCountOfOrgOrSiteUnmatched] = useState(0);
+
const countOfDeviceFacts = Object.keys(deviceFacts).length;
const countOfAdoptedDevices = Object.values(deviceFacts).filter(
(facts) => cloudDevices[facts?.serialNumber]
).length;
+ const doesSiteNameExist = (orgName, siteName) => {
+ const org = cloudInventory.find((item) => item.name === orgName);
+
+ // If the organization is not found, return false
+ if (!org) {
+ return false;
+ }
+
+ // Check if the site name exists within the organization's sites array
+ const siteExists = org.sites.some((site) => site.name === siteName);
+
+ return siteExists;
+ };
+
+ const countNonMatchingInventoryItems = () => {
+ let nonMatchingCount = 0;
+ inventory.forEach((item) => {
+ const existence = doesSiteNameExist(item.organization, item.site);
+ if (!existence) {
+ nonMatchingCount++;
+ }
+ });
+
+ return nonMatchingCount;
+ }
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ const count = countNonMatchingInventoryItems();
+ setCountOfOrgOrSiteUnmatched(count);
+ }, 3000); // 3 seconds delay
+
+ return () => clearTimeout(timer); // Cleanup timer on component unmount
+ }, [inventory, cloudDevices]);
+
return (
{
>
Adopted Devices: {countOfAdoptedDevices}
+ {isUserLoggedIn && countOfOrgOrSiteUnmatched > 0 && (
+
+ Devices with unmatched organization or site: {countOfOrgOrSiteUnmatched}
+
+ )}
);
diff --git a/jccm/src/Frontend/Layout/GlobalSettings/GeneralCard.js b/jccm/src/Frontend/Layout/GlobalSettings/GeneralCard.js
index 148e0d6..31d52d6 100644
--- a/jccm/src/Frontend/Layout/GlobalSettings/GeneralCard.js
+++ b/jccm/src/Frontend/Layout/GlobalSettings/GeneralCard.js
@@ -39,6 +39,7 @@ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
export const GeneralCard = () => {
const { settings, setSettings, importSettings, exportSettings } = useStore();
const [jsiTerm, setJsiTerm] = useState(false);
+ const [deleteOutboundSSHTerm, setDeleteOutboundSSHTerm] = useState(false);
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
@@ -49,7 +50,10 @@ export const GeneralCard = () => {
const fetchData = async () => {
importSettings();
await delay(300);
+
+ console.log('importing settings', settings);
setJsiTerm(settings?.jsiTerm ? true : false);
+ setDeleteOutboundSSHTerm(settings?.deleteOutboundSSHTerm ? true : false);
};
fetchData();
}, []);
@@ -63,15 +67,38 @@ export const GeneralCard = () => {
saveFunction();
};
- const handleActive = async (event) => {
+ const saveDeleteOutboundSSHTerm = (newDeleteOutboundSSHTerm) => {
+ const saveFunction = async () => {
+ const newSettings = { ...settings, deleteOutboundSSHTerm: newDeleteOutboundSSHTerm };
+ setSettings(newSettings);
+ exportSettings(newSettings);
+ };
+ saveFunction();
+ };
+
+ const onChangeJsiTerm = async (event) => {
const checked = event.currentTarget.checked;
setJsiTerm(checked);
-
saveJsiTerm(checked);
};
+ const onChangeDeleteOutboundSSHTerm = async (event) => {
+ const checked = event.currentTarget.checked;
+ setDeleteOutboundSSHTerm(checked);
+ saveDeleteOutboundSSHTerm(checked);
+ };
+
return (
-
+
{
>
{jsiTerm ? 'Enabled' : 'Disabled'}
+
+
+
Override outbound SSH config during adoption:
+
+
+
+
+
{deleteOutboundSSHTerm ? 'Enabled' : 'Disabled'}
+
);
};
diff --git a/jccm/src/Frontend/Layout/InventoryLocalEditForm.js b/jccm/src/Frontend/Layout/InventoryLocalEditForm.js
index 126b211..8c87d7c 100644
--- a/jccm/src/Frontend/Layout/InventoryLocalEditForm.js
+++ b/jccm/src/Frontend/Layout/InventoryLocalEditForm.js
@@ -51,6 +51,7 @@ const { electronAPI } = window;
import * as Constants from '../Common/CommonVariables';
import useStore from '../Common/StateStore';
import { useNotify } from '../Common/NotificationContext';
+import eventBus from '../Common/eventBus';
const Dismiss = bundleIcon(DismissFilled, DismissRegular);
const AddCircle = bundleIcon(AddCircleFilled, AddCircleRegular);
@@ -186,7 +187,8 @@ const InventoryLocalEditForm = ({ isOpen, onClose, title, importedInventory }) =
const onSave = async () => {
setInventory(rowData);
await electronAPI.saSetLocalInventory({ inventory: rowData });
- setTimeout(() => {
+ setTimeout(async () => {
+ await eventBus.emit('device-facts-cleanup', { notification: false });
onClose();
}, 300);
};
diff --git a/jccm/src/Frontend/Layout/InventoryTreeMenuLocal.js b/jccm/src/Frontend/Layout/InventoryTreeMenuLocal.js
index c3723b2..6687633 100644
--- a/jccm/src/Frontend/Layout/InventoryTreeMenuLocal.js
+++ b/jccm/src/Frontend/Layout/InventoryTreeMenuLocal.js
@@ -106,7 +106,6 @@ import {
RenameFilled,
DeleteDismissRegular,
DeleteDismissFilled,
- WarningRegular,
CutRegular,
CutFilled,
ClipboardPasteRegular,
@@ -162,6 +161,13 @@ import {
BoxToolboxRegular,
EqualOffRegular,
EqualOffFilled,
+ FlagOffFilled,
+ OrganizationFilled,
+ WarningFilled,
+ WarningRegular,
+ ErrorCircleRegular,
+ FlagFilled,
+ WeatherThunderstormRegular,
bundleIcon,
} from '@fluentui/react-icons';
import _ from 'lodash';
@@ -321,7 +327,7 @@ const convertToFlatTreeItems = (localInventory) => {
const InventoryTreeMenuLocal = () => {
const { showContextMenu } = useContextMenu();
const { notify } = useNotify();
- const { isUserLoggedIn, settings } = useStore();
+ const { isUserLoggedIn, orgs, settings } = useStore();
const { tabs, addTab, setSelectedTabValue, adoptConfig, inventory, setInventory } = useStore();
const { cloudInventory, setCloudInventory, setCloudInventoryFilterApplied, cloudDevices } = useStore();
@@ -391,11 +397,10 @@ const InventoryTreeMenuLocal = () => {
const [isSiteMatch, setIsSiteMatch] = useState(true);
useEffect(() => {
- // const isFact = !!device?.facts;
const isFact = !!deviceFacts[device._path];
- const adopted = isFact ? !!cloudDevices[deviceFacts[device._path].serialNumber] : false;
+ const adopted = isFact ? !!cloudDevices[deviceFacts[device._path]?.serialNumber] : false;
if (adopted) {
- const cloudDevice = cloudDevices[deviceFacts[device._path].serialNumber];
+ const cloudDevice = cloudDevices[deviceFacts[device._path]?.serialNumber];
const cloudOrgName = cloudDevice.org_name;
const cloudSiteName = cloudDevice.site_name;
const deviceOrgName = device.orgName;
@@ -403,57 +408,130 @@ const InventoryTreeMenuLocal = () => {
setIsOrgMatch(cloudOrgName === deviceOrgName);
setIsSiteMatch(cloudSiteName === deviceSiteName);
-
- if (cloudOrgName !== deviceOrgName) {
- console.log('>>>device is not adopted to same org', cloudDevice, device);
- }
-
- if (cloudSiteName !== deviceSiteName) {
- console.log('>>>device is not adopted to same site', cloudDevice, device);
- }
-
- // console.log('device', device);
- // console.log('cloudDevices', cloudDevices);
- // console.log('adopted: cloudDevices: ', cloudDevices[deviceFacts[device._path].serialNumber])
- // console.log('adopted: deviceFacts: ', deviceFacts[device._path])
}
setIsAdopted(adopted);
}, [cloudDevices, device]);
+ const IconWithTooltip = ({ content, relationship, Icon, size, color }) => (
+
+
+
+
+
+ );
+
+ const CircleIconWithTooltip = ({ content, relationship, Icon, color, size = '10px' }) => (
+
+
+
+
+
+ );
+
+ const orgMismatchContent = (
+
+
+ The organization name ({device.orgName} ) does not exist in your account:
+
+
+ {`"${device.orgName}" ≠ "${cloudDevices[deviceFacts[device._path]?.serialNumber]?.org_name}"`}
+
+
+ );
+
+ const siteMismatchContent = (
+
+
+ The site name ({device.siteName} ) does not exist in your account:
+
+
+ {`"${device.siteName}" ≠ "${cloudDevices[deviceFacts[device._path]?.serialNumber]?.site_name}"`}
+
+
+ );
+
const getIcon = () => {
if (isAdopted) {
return (
-
- {renderObjectValue(cloudDevices[deviceFacts[device._path].serialNumber])}
-
- }
- relationship='description'
- withArrow
- positioning='above-end'
+
- {isOpen ? (
-
- ) : (
-
+ ) : !isSiteMatch ? (
+
+ ) : null}
+
+
+ {renderObjectValue(cloudDevices[deviceFacts[device._path]?.serialNumber])}
+
+ }
+ relationship='description'
+ withArrow
+ positioning='above-end'
+ >
+ {isOpen ? (
+
+ ) : (
- {/* {!isOrgMatch && (
-
- )} */}
-
- )}
-
+ )}
+
+
);
} else {
return isOpen ? (
@@ -728,14 +806,14 @@ const InventoryTreeMenuLocal = () => {
await electronAPI.saSaveDeviceFacts({ facts: deviceFactsRef.current });
};
- const actionAdoptDevice = async (device, jsiTerm = false) => {
+ const actionAdoptDevice = async (device, jsiTerm = false, deleteOutboundSSHTerm = false) => {
const maxRetries = 6;
const retryInterval = 15 * 1000; // 15 seconds in milliseconds
setIsAdopting(device._path, { status: true, retry: 0 });
for (let attempt = 1; attempt <= maxRetries; attempt++) {
- const result = await adoptDevices(device, jsiTerm);
+ const result = await adoptDevices(device, jsiTerm, deleteOutboundSSHTerm);
if (result.status) {
setTimeout(async () => {
const fetchAndUpdateCloudInventory = async () => {
@@ -784,9 +862,15 @@ const InventoryTreeMenuLocal = () => {
};
});
- const targetDevices = inventoryWithPath.filter(
- (device) => device.path.startsWith(node.value) && !!!cloudDevices[device?.facts?.serialNumber]
- );
+ const targetDevices = inventoryWithPath.filter((device) => {
+ const orgName = device.organization;
+ const siteName = device.site;
+
+ const siteExists = doesSiteNameExist(orgName, siteName);
+ const serialNumber = deviceFacts[device.path]?.serialNumber;
+
+ return siteExists && device.path.startsWith(node.value) && !!!cloudDevices[serialNumber];
+ });
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const rateLimit = 1000 / rate; // Rate in calls per second
@@ -798,7 +882,7 @@ const InventoryTreeMenuLocal = () => {
const executeCall = async (device) => {
const promise = new Promise(async (resolve) => {
- await actionAdoptDevice(device, jsiTerm);
+ await actionAdoptDevice(device, jsiTerm, settings.deleteOutboundSSHTerm);
resolve();
}).then(() => {
runningCalls--;
@@ -828,9 +912,10 @@ const InventoryTreeMenuLocal = () => {
const actionReleaseDevice = async (device) => {
setIsReleasing(device.path, true);
- const serialNumber = deviceFacts[device._path].serialNumber;
+ const serialNumber = deviceFacts[device.path]?.serialNumber;
+ const organization = cloudDevices[serialNumber]?.org_name;
- const result = await releaseDevices(device, serialNumber);
+ const result = await releaseDevices({ organization, serialNumber });
if (result.status) {
setTimeout(async () => {
const fetchAndUpdateCloudInventory = async () => {
@@ -871,9 +956,10 @@ const InventoryTreeMenuLocal = () => {
};
});
- const targetDevices = inventoryWithPath.filter(
- (device) => device.path.startsWith(node.value) && !!cloudDevices[deviceFacts[device._path].serialNumber]
- );
+ const targetDevices = inventoryWithPath.filter((device) => {
+ const serialNumber = deviceFacts[device.path]?.serialNumber;
+ return device.path.startsWith(node.value) && !!cloudDevices[serialNumber];
+ });
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const rateLimit = 1000 / rate; // Rate in calls per second
@@ -916,7 +1002,7 @@ const InventoryTreeMenuLocal = () => {
const devices = inventory.filter(
(device) => device._path.startsWith(node.value) && !!deviceFacts[device._path]
);
- const devicesAdopted = devices.filter((device) => !!cloudDevices[deviceFacts[device._path].serialNumber]);
+ const devicesAdopted = devices.filter((device) => !!cloudDevices[deviceFacts[device._path]?.serialNumber]);
const isTargetDeviceAvailable = () => {
return devices.length > 0;
@@ -926,6 +1012,26 @@ const InventoryTreeMenuLocal = () => {
return devicesAdopted.length > 0;
};
+ const isOrgSiteMatch = () => {
+ const splitParentValue = node.parentValue ? node.parentValue.split('/') : [];
+
+ switch (node.type) {
+ case 'root':
+ return true;
+ case 'org':
+ return doesOrgNameExist(node.content);
+ case 'site':
+ return doesSiteNameExist(splitParentValue.pop(), node.content);
+ case 'device':
+ return doesSiteNameExist(
+ splitParentValue[splitParentValue.length - 2],
+ splitParentValue[splitParentValue.length - 1]
+ );
+ default:
+ return false;
+ }
+ };
+
return (
@@ -952,7 +1058,7 @@ const InventoryTreeMenuLocal = () => {
)}
}
onClick={async () => {
actionAdoptDevices(node);
@@ -968,7 +1074,7 @@ const InventoryTreeMenuLocal = () => {
{settings.jsiTerm && (
}
onClick={async () => {
actionAdoptDevices(node, true);
@@ -1024,6 +1130,30 @@ const InventoryTreeMenuLocal = () => {
});
const treeProps = flatTree.getTreeProps();
+ const doesOrgNameExist = (orgName) => {
+ // Iterate over the values of the orgs object
+ for (const name of Object.values(orgs)) {
+ if (name === orgName) {
+ return true; // Return true if the orgName exists
+ }
+ }
+ return false; // Return false if the orgName does not exist
+ };
+
+ const doesSiteNameExist = (orgName, siteName) => {
+ const org = cloudInventory.find((item) => item.name === orgName);
+
+ // If the organization is not found, return false
+ if (!org) {
+ return false;
+ }
+
+ // Check if the site name exists within the organization's sites array
+ const siteExists = org.sites.some((site) => site.name === siteName);
+
+ return siteExists;
+ };
+
return (
{
aside={ }
onContextMenu={(event) => onNodeRightClick(event, rowData)}
>
- {rowData.content}
+ {rowData.content}
) : rowData.type === 'org' ? (
@@ -1061,7 +1191,24 @@ const InventoryTreeMenuLocal = () => {
iconBefore={rowData.icon}
onContextMenu={(event) => onNodeRightClick(event, rowData)}
>
- {rowData.content}
+ {!isUserLoggedIn || doesOrgNameExist(rowData.content) ? (
+ {rowData.content}
+ ) : (
+
+
+ {rowData.content}
+
+
+ )}
) : rowData.type === 'site' ? (
@@ -1075,7 +1222,25 @@ const InventoryTreeMenuLocal = () => {
iconBefore={rowData.icon}
onContextMenu={(event) => onNodeRightClick(event, rowData)}
>
- {rowData.content}
+ {!isUserLoggedIn ||
+ doesSiteNameExist(rowData.parentValue.split('/').pop(), rowData.content) ? (
+ {rowData.content}
+ ) : (
+
+
+ {rowData.content}
+
+
+ )}
) : rowData.type === 'device' ? (
diff --git a/jccm/src/Frontend/MainEventProcessor.js b/jccm/src/Frontend/MainEventProcessor.js
index e56f9b9..28d7bfd 100644
--- a/jccm/src/Frontend/MainEventProcessor.js
+++ b/jccm/src/Frontend/MainEventProcessor.js
@@ -16,7 +16,7 @@ export const MainEventProcessor = () => {
const { isUserLoggedIn, setIsUserLoggedIn, user, setUser } = useStore();
const { inventory, setInventory } = useStore();
const { cloudInventory, setCloudInventory } = useStore();
- const { deviceFacts, setDeviceFactsAll, setDeviceFacts, deleteDeviceFacts, zeroDeviceFacts } = useStore();
+ const { deviceFacts, setDeviceFactsAll, cleanUpDeviceFacts, zeroDeviceFacts } = useStore();
const { cloudInventoryFilterApplied, setCloudInventoryFilterApplied } = useStore();
const { currentActiveThemeName, setCurrentActiveThemeName } = useStore();
@@ -137,11 +137,13 @@ export const MainEventProcessor = () => {
}
} else {
setUser(null);
+ setCloudInventory([])
setIsUserLoggedIn(false);
setCurrentActiveThemeName(data.theme);
}
} catch (error) {
setUser(null);
+ setCloudInventory([])
setIsUserLoggedIn(false);
console.error('Session check error:', error);
}
@@ -158,11 +160,17 @@ export const MainEventProcessor = () => {
}
};
+ const handleDeviceFactsCleanup = async () => {
+ console.log('handleDeviceFactsCleanup');
+ cleanUpDeviceFacts();
+ };
+
eventBus.on('local-inventory-refresh', handleLocalInventoryRefresh);
eventBus.on('cloud-inventory-refresh', handleCloudInventoryRefresh);
eventBus.on('reset-device-facts', handleResetDeviceFacts);
eventBus.on('user-session-check', handleUserSessionCheck);
eventBus.on('device-facts-refresh', handleDeviceFactsRefresh);
+ eventBus.on('device-facts-cleanup', handleDeviceFactsCleanup);
return () => {
eventBus.off('local-inventory-refresh', handleLocalInventoryRefresh);
@@ -170,6 +178,7 @@ export const MainEventProcessor = () => {
eventBus.off('reset-device-facts', handleResetDeviceFacts);
eventBus.off('user-session-check', handleUserSessionCheck);
eventBus.off('device-facts-refresh', handleDeviceFactsRefresh);
+ eventBus.off('device-facts-cleanup', handleDeviceFactsCleanup);
};
}, []);
diff --git a/jccm/src/Services/ApiServer.js b/jccm/src/Services/ApiServer.js
index 7e96582..42af25b 100644
--- a/jccm/src/Services/ApiServer.js
+++ b/jccm/src/Services/ApiServer.js
@@ -381,7 +381,8 @@ export const setupApiHandlers = () => {
ipcMain.handle('saAdoptDevice', async (event, args) => {
console.log('main: saAdoptDevice');
- const { organization, site, address, port, username, password, jsiTerm, ...others } = args;
+ const { organization, site, address, port, username, password, jsiTerm, deleteOutboundSSHTerm, ...others } =
+ args;
const cloudOrgs = await msGetCloudOrgs();
const orgId = cloudOrgs[organization]?.id;
@@ -393,10 +394,13 @@ export const setupApiHandlers = () => {
endpoint = 'jsi/devices';
}
-
const api = `orgs/${orgId}/${endpoint}/outbound_ssh_cmd${siteId ? `?site_id=${siteId}` : ''}`;
const response = await acRequest(api, 'GET', null);
- const configCommand = `${response.cmd}\n`;
+
+ const configCommand = deleteOutboundSSHTerm
+ ? `delete system services outbound-ssh\n${response.cmd}\n`
+ : `${response.cmd}\n`;
+
const reply = await commitJunosSetConfig(address, port, username, password, configCommand);
if (reply.status === 'success' && reply.data.includes(' ')) {