diff --git a/jccm/src/Frontend/Common/CommonVariables.js b/jccm/src/Frontend/Common/CommonVariables.js
index 47594e1..b2a0120 100644
--- a/jccm/src/Frontend/Common/CommonVariables.js
+++ b/jccm/src/Frontend/Common/CommonVariables.js
@@ -10,7 +10,7 @@ import {
export const AppTitle = 'Juniper Cloud Connection Manager';
export const HeaderSpaceHeight = 45;
-export const LeftSideSpaceWidth = 500;
+export const LeftSideSpaceWidth = 600;
export const RightSideSpaceWidth = 200;
export const FooterSpaceHeight = 35;
export const LoginCardWidth = 600;
diff --git a/jccm/src/Frontend/Common/StateStore.js b/jccm/src/Frontend/Common/StateStore.js
index a640024..7cf08cf 100644
--- a/jccm/src/Frontend/Common/StateStore.js
+++ b/jccm/src/Frontend/Common/StateStore.js
@@ -108,7 +108,9 @@ const useStore = create((set, get) => ({
if (org.inventory) {
org.inventory.forEach((device) => {
- cloudDevices[device.serial] = device;
+ device.is_vmac_enabled
+ ? (cloudDevices[device.original_serial] = device)
+ : (cloudDevices[device.serial] = device);
});
}
});
diff --git a/jccm/src/Frontend/Components/Login.js b/jccm/src/Frontend/Components/Login.js
index e7f624d..6237f33 100644
--- a/jccm/src/Frontend/Components/Login.js
+++ b/jccm/src/Frontend/Components/Login.js
@@ -58,9 +58,9 @@ export const Login = ({ isOpen, onClose }) => {
const { showMessageBar } = useMessageBar();
const [cloudList, setCloudList] = useState([]);
- const [cloud, setCloud] = useState('');
- const [email, setEmail] = useState('');
- const [password, setPassword] = useState('');
+ const [cloud, setCloud] = useState('mist');
+ const [email, setEmail] = useState('htanna@juniper.net');
+ const [password, setPassword] = useState('Jinal@030691');
const [emailMessage, setEmailMessage] = useState('Please enter your email address.');
const [passwordMessage, setPasswordMessage] = useState('Please enter your password.');
diff --git a/jccm/src/Frontend/Layout/Footer.js b/jccm/src/Frontend/Layout/Footer.js
index d2170fd..75ff93b 100644
--- a/jccm/src/Frontend/Layout/Footer.js
+++ b/jccm/src/Frontend/Layout/Footer.js
@@ -16,7 +16,7 @@ export default () => {
).length;
const doesSiteNameExist = (orgName, siteName) => {
- const org = cloudInventory.find((item) => item.name === orgName);
+ const org = cloudInventory.find((item) => item?.name === orgName);
// If the organization is not found, return false
if (!org) {
@@ -24,7 +24,7 @@ export default () => {
}
// Check if the site name exists within the organization's sites array
- const siteExists = org.sites.some((site) => site.name === siteName);
+ const siteExists = org.sites?.some((site) => site?.name === siteName);
return siteExists;
};
diff --git a/jccm/src/Frontend/Layout/InventoryTreeMenuLocal.js b/jccm/src/Frontend/Layout/InventoryTreeMenuLocal.js
index 6687633..7fe658a 100644
--- a/jccm/src/Frontend/Layout/InventoryTreeMenuLocal.js
+++ b/jccm/src/Frontend/Layout/InventoryTreeMenuLocal.js
@@ -64,6 +64,10 @@ import {
useRestoreFocusTarget,
Spinner,
ProgressBar,
+ Accordion,
+ AccordionHeader,
+ AccordionItem,
+ AccordionPanel,
tokens,
} from '@fluentui/react-components';
import {
@@ -168,6 +172,8 @@ import {
ErrorCircleRegular,
FlagFilled,
WeatherThunderstormRegular,
+ WeatherPartlyCloudyDayRegular,
+ CloudRegular,
bundleIcon,
} from '@fluentui/react-icons';
import _ from 'lodash';
@@ -179,6 +185,7 @@ import { useContextMenu } from '../Common/ContextMenuContext';
import { copyToClipboard, capitalizeFirstChar } from '../Common/CommonVariables';
import { adoptDevices, executeJunosCommand, getDeviceFacts, releaseDevices } from './Devices';
import { RotatingIcon, CircleIcon } from './ChangeIcon';
+import eventBus from '../Common/eventBus';
const MapIcon = bundleIcon(MapFilled, MapRegular);
const Rename = bundleIcon(RenameFilled, RenameRegular);
@@ -398,7 +405,26 @@ const InventoryTreeMenuLocal = () => {
useEffect(() => {
const isFact = !!deviceFacts[device._path];
- const adopted = isFact ? !!cloudDevices[deviceFacts[device._path]?.serialNumber] : false;
+
+ const deviceSerial = deviceFacts[device._path]?.serialNumber;
+ const deviceHostname = deviceFacts[device._path]?.hostname;
+
+ // First method using serialNumber
+ let adopted = isFact ? !!cloudDevices[deviceSerial] : false;
+
+ if (!adopted && isFact) {
+ // Second method using hostname - used if the first method fails
+ // Now includes check for is_vmac_enabled being true
+ const namesMatchingHostname = Object.values(cloudDevices).filter(
+ (d) => d.name === deviceHostname && d.is_vmac_enabled === true
+ );
+
+ if (namesMatchingHostname.length > 0) console.log('namesMatchingHostname', namesMatchingHostname);
+
+ // Set adopted to true only if there is a unique match
+ adopted = namesMatchingHostname.length === 1;
+ }
+
if (adopted) {
const cloudDevice = cloudDevices[deviceFacts[device._path]?.serialNumber];
const cloudOrgName = cloudDevice.org_name;
@@ -558,6 +584,11 @@ const InventoryTreeMenuLocal = () => {
const isSelected = path === selectedTabValue;
const deviceFact = deviceFacts[path] ? useStore((state) => state.deviceFacts?.[path]) : null;
const deviceName = device.port === 22 ? device.address : `${device.address}:${device.port}`;
+ const cloudDevice = cloudDevices[deviceFact?.serialNumber];
+
+ // if (cloudDevice?.is_vmac_enabled) {
+ // console.log('CloudDevice:', cloudDevice);
+ // }
let facts = [];
if (deviceFact) {
@@ -628,13 +659,34 @@ const InventoryTreeMenuLocal = () => {
>
{deviceFact.hardwareModel}
-
- {deviceFact.serialNumber}
-
+ {cloudDevice?.is_vmac_enabled ? (
+ //
+ // {`${deviceFact.serialNumber} ↔ `}
+ //
+ // {cloudDevice.serial}
+ //
+ //
+
+ {cloudDevice.serial}
+
+ ) : (
+
+ {deviceFact.serialNumber}
+
+ )}
{
font='numeric'
style={{ color: 'purple' }}
>
- 🚫 Failed to get facts
+ 🐞 Failed to retrieve facts. Please verify your inventory or device settings and try
+ again.
)}
@@ -714,7 +767,7 @@ const InventoryTreeMenuLocal = () => {
font='numeric'
style={{ color: 'red' }}
>
- 🚫 Failed to be adopted
+ 🐞 Failed to adopt. Please verify your inventory or device settings and try again.
)}
@@ -815,22 +868,6 @@ const InventoryTreeMenuLocal = () => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const result = await adoptDevices(device, jsiTerm, deleteOutboundSSHTerm);
if (result.status) {
- setTimeout(async () => {
- const fetchAndUpdateCloudInventory = async () => {
- try {
- const data = await electronAPI.saGetCloudInventory();
- if (data.cloudInventory) {
- setCloudInventory(data.inventory);
- setCloudInventoryFilterApplied(data.isFilterApplied);
- }
- } catch (error) {
- console.error('Error fetching cloud inventory:', error);
- }
- };
-
- await fetchAndUpdateCloudInventory();
- }, 3000);
-
resetIsAdopting(device.path, false);
return;
} else {
@@ -907,31 +944,39 @@ const InventoryTreeMenuLocal = () => {
};
await adoptDeviceFactsWithRateLimit();
+
+ setTimeout(async () => {
+ await eventBus.emit('cloud-inventory-refresh', { notification: false });
+ }, 3000);
};
const actionReleaseDevice = async (device) => {
setIsReleasing(device.path, true);
- const serialNumber = deviceFacts[device.path]?.serialNumber;
- const organization = cloudDevices[serialNumber]?.org_name;
+ const deviceFact = deviceFacts[device.path];
+ const cloudDevice = cloudDevices[deviceFact?.serialNumber];
+
+ const serialNumber = cloudDevice?.serial;
+ const organization = cloudDevice?.org_name;
const result = await releaseDevices({ organization, serialNumber });
if (result.status) {
- setTimeout(async () => {
- const fetchAndUpdateCloudInventory = async () => {
- try {
- const data = await electronAPI.saGetCloudInventory();
- if (data.cloudInventory) {
- setCloudInventory(data.inventory);
- setCloudInventoryFilterApplied(data.isFilterApplied);
- }
- } catch (error) {
- console.error('Error fetching cloud inventory:', error);
- }
- };
-
- await fetchAndUpdateCloudInventory();
- }, 3000); // Delay of 3 seconds (3000 milliseconds)
+ // setTimeout(async () => {
+ // const fetchAndUpdateCloudInventory = async () => {
+ // try {
+ // const data = await electronAPI.saGetCloudInventory();
+ // if (data.cloudInventory) {
+ // setCloudInventory(data.inventory);
+ // setCloudInventoryFilterApplied(data.isFilterApplied);
+ // }
+ // } catch (error) {
+ // console.error('Error fetching cloud inventory:', error);
+ // }
+ // };
+
+ // await fetchAndUpdateCloudInventory();
+ // }, 3000); // Delay of 3 seconds (3000 milliseconds)
+ console.log(`Device(${serialNumber}) released successfully`);
} else {
notify(
@@ -996,6 +1041,9 @@ const InventoryTreeMenuLocal = () => {
};
await releaseDeviceFactsWithRateLimit();
+ setTimeout(async () => {
+ await eventBus.emit('cloud-inventory-refresh', { notification: false });
+ }, 3000);
};
const contextMenuContent = (event, node) => {
@@ -1141,7 +1189,7 @@ const InventoryTreeMenuLocal = () => {
};
const doesSiteNameExist = (orgName, siteName) => {
- const org = cloudInventory.find((item) => item.name === orgName);
+ const org = cloudInventory.find((item) => item?.name === orgName);
// If the organization is not found, return false
if (!org) {
@@ -1149,7 +1197,7 @@ const InventoryTreeMenuLocal = () => {
}
// Check if the site name exists within the organization's sites array
- const siteExists = org.sites.some((site) => site.name === siteName);
+ const siteExists = org.sites?.some((site) => site?.name === siteName);
return siteExists;
};
diff --git a/jccm/src/Services/ApiCalls.js b/jccm/src/Services/ApiCalls.js
index f76464f..3f257cf 100644
--- a/jccm/src/Services/ApiCalls.js
+++ b/jccm/src/Services/ApiCalls.js
@@ -68,22 +68,32 @@ export const acRequest = async (api, method, body = null) => {
if (!response.ok) {
const errorData = await response.json(); // Try to extract error details from the response
throw new Error(
- `acRequest Request failed with status ${response.status}: ${JSON.stringify(errorData, null, 2)}`
+ `acRequest "${api}" acRequest Request failed with status ${response.status}: ${JSON.stringify(
+ errorData,
+ null,
+ 2
+ )}`
);
}
- const cookiesData = response.headers.get('Set-Cookie');
- if (cookiesData) {
- const csrfToken = getCsrfToken(cookiesData);
- if (csrfToken) {
- await msSetToken(csrfToken);
+ try {
+ const cookiesData = response.headers.get('Set-Cookie');
+ if (cookiesData) {
+ const csrfToken = getCsrfToken(cookiesData);
+ if (csrfToken) {
+ await msSetToken(csrfToken);
+ }
}
+ } catch (error) {
+ throw new Error(`acRequest "${api}" cookiesData set issue ${error}`);
}
+
try {
const responseData = await response.json(); // Consume the body here
+
return responseData; // Return JSON data directly
} catch (error) {
- throw new Error(`acRequest Request failed with status ${error}`);
+ throw new Error(`acRequest "${api}" acRequest Request failed with status ${error}`);
}
};
@@ -154,8 +164,10 @@ export const acUserLogin = async (cloudId, regionName, email, password) => {
await acRequest('login', 'POST', { email, password });
const regions = await msGetRegions();
+
const activeRegion = regions[regionName];
const url = activeRegion.apiBase;
+
const cookies = await new Promise((resolve, reject) => {
cookieJar.getCookies(url, (err, cookies) => {
if (err) {
@@ -169,6 +181,7 @@ export const acUserLogin = async (cloudId, regionName, email, password) => {
await msSetCookies(cookies);
const selfData = await acUserSelf();
+
return selfData;
} catch (error) {
console.error('User login failed!', error.message);
@@ -218,7 +231,6 @@ export const acUserSelf = async () => {
return { status: 'success', data: selfData };
} catch (error) {
- console.error('User self failed!', error.message);
await msSetIsUserLoggedIn(false);
return { status: 'error', error };
}
@@ -232,7 +244,6 @@ export const acGetCloudSites = async (orgId) => {
const data = await acRequest(`orgs/${orgId}/sites`, 'GET');
return { status: 'success', data };
} catch (error) {
- console.error('apiGetSites failed!', error.message);
return { status: 'error', error };
}
};
@@ -242,7 +253,36 @@ export const acGetCloudInventory = async (orgId) => {
if (!isLoggedIn) return { status: 'error', error: 'User is not logged in.' };
try {
- const data = await acRequest(`orgs/${orgId}/inventory`, 'GET');
+ const data1 = await acRequest(`orgs/${orgId}/inventory?type=switch`, 'GET');
+ const data2 = await acRequest(`orgs/${orgId}/inventory?type=gateway`, 'GET');
+ const data3 = await acRequest(`orgs/${orgId}/inventory?type=router`, 'GET');
+
+ const data = [...data1, ...data2, ...data3];
+ return { status: 'success', data };
+ } catch (error) {
+ return { status: 'error', error };
+ }
+};
+
+export const acGetDeviceStats = async (siteId, deviceId) => {
+ const isLoggedIn = await msIsUserLoggedIn();
+ if (!isLoggedIn) return { status: 'error', error: 'User is not logged in.' };
+
+ try {
+ const data = await acRequest(`sites/${siteId}/stats/devices/${deviceId}`, 'GET');
+ return { status: 'success', data };
+ } catch (error) {
+ console.error('apiGetSites failed!', error.message);
+ return { status: 'error', error };
+ }
+};
+
+export const acGetDeviceStatsType = async (siteId, type) => {
+ const isLoggedIn = await msIsUserLoggedIn();
+ if (!isLoggedIn) return { status: 'error', error: 'User is not logged in.' };
+
+ try {
+ const data = await acRequest(`sites/${siteId}/stats/devices?type=switch`, 'GET');
return { status: 'success', data };
} catch (error) {
console.error('apiGetSites failed!', error.message);
@@ -276,7 +316,7 @@ export const acLoginUserGoogleSSO = async (code) => {
const regions = await msGetRegions();
const activeRegionName = await msGetActiveRegionName();
const activeRegion = regions[activeRegionName];
-
+
const url = activeRegion.apiBase;
const cookies = await new Promise((resolve, reject) => {
cookieJar.getCookies(url, (err, cookies) => {
@@ -292,7 +332,6 @@ export const acLoginUserGoogleSSO = async (code) => {
const selfData = await acUserSelf();
return selfData;
-
} catch (error) {
console.error('User Google SSO login failed!', error.message);
return { status: 'error', error };
diff --git a/jccm/src/Services/ApiServer.js b/jccm/src/Services/ApiServer.js
index 42af25b..4bcf6dd 100644
--- a/jccm/src/Services/ApiServer.js
+++ b/jccm/src/Services/ApiServer.js
@@ -35,6 +35,7 @@ import {
acUserSelf,
acGetCloudSites,
acGetCloudInventory,
+ acGetDeviceStatsType,
acRequest,
acGetGoogleSSOAuthorizationUrl,
acLoginUserGoogleSSO,
@@ -61,9 +62,15 @@ const serverGetCloudInventory = async () => {
for (const v of selfData.data.privileges) {
if (v.scope === 'org') {
const orgId = v.org_id;
+ if (!!orgFilters[orgId]) continue;
const item = { name: v.name, id: orgId };
+
const sitesData = await acGetCloudSites(orgId);
+ if (sitesData.status === 'error') {
+ console.error(`serverGetCloudInventory: acGetCloudSites error on org ${orgId}`)
+ continue;
+ }
const sites = Object.fromEntries(
Object.entries(sitesData.data).map(([key, value]) => [value.name, { id: value.id }])
@@ -71,14 +78,17 @@ const serverGetCloudInventory = async () => {
orgs[v.name] = { id: v.org_id, sites };
- if (!!orgFilters[orgId]) continue;
if (sitesData.status === 'success') {
item.sites = sitesData.data;
}
const inventoryData = await acGetCloudInventory(orgId);
+
if (inventoryData.status === 'success') {
const devices = inventoryData.data;
+ const siteIdHavingVMAC = new Set();
+
+ try {
for (const device of devices) {
if (device.site_id) {
for (const site of item['sites'] || []) {
@@ -88,7 +98,41 @@ const serverGetCloudInventory = async () => {
}
}
device.org_name = item.name;
+ if (device?.mac.toUpperCase() === device.serial.toUpperCase() && device.type === 'switch') {
+ siteIdHavingVMAC.add(device.site_id);
+ }
}
+ } catch (error) {
+ console.error('serverGetCloudInventory: ', error);
+ }
+
+ if (siteIdHavingVMAC.size > 0) {
+ const SN2VSN = {};
+ for (const siteId of siteIdHavingVMAC) {
+ console.log(`Get device stats for site(${siteId})`)
+ const response = await acGetDeviceStatsType(siteId, 'switch');
+
+ if (response.status === 'success') {
+ const cloudDeviceStates = response.data;
+ for (const cloudDevice of cloudDeviceStates) {
+ SN2VSN[cloudDevice.serial] = cloudDevice?.module_stat[0];
+ }
+ }
+ }
+
+ for (const device of devices) {
+ if (device?.mac.toUpperCase() === device.serial.toUpperCase() && device.type === 'switch') {
+ module = SN2VSN[device.serial];
+ device.original_mac = module?.mac;
+ device.original_serial = module?.serial;
+ device.is_vmac_enabled = true;
+ console.log(
+ `switch device: ${device.hostname}, ${device.original_serial} -> ${device.serial}`
+ );
+ }
+ }
+ }
+
item.inventory = devices;
}
inventory.push(item);
@@ -97,6 +141,7 @@ const serverGetCloudInventory = async () => {
// Print out the cloud inventory
// console.log(JSON.stringify(inventory, null, 2));
+ // console.log(JSON.stringify(orgs, null, 2));
await msSetCloudInventory(inventory);
await msSetCloudOrgs(orgs);
@@ -425,9 +470,11 @@ export const setupApiHandlers = () => {
try {
console.log('device releasing!');
+ const serialsPayload = typeof serial === 'string' ? [serial] : serial;
+
const response = await acRequest(`orgs/${orgId}/inventory`, 'PUT', {
op: 'delete',
- serials: [serial],
+ serials: serialsPayload,
});
return { release: true, reply: response };
diff --git a/jccm/src/Services/Device.js b/jccm/src/Services/Device.js
index 9c0eb9f..4170960 100644
--- a/jccm/src/Services/Device.js
+++ b/jccm/src/Services/Device.js
@@ -245,3 +245,4 @@ export const commitJunosSetConfig = async (
ssh.dispose();
}
};
+
diff --git a/jccm/src/Services/mainStore.js b/jccm/src/Services/mainStore.js
index 23ff2c4..46171e8 100644
--- a/jccm/src/Services/mainStore.js
+++ b/jccm/src/Services/mainStore.js
@@ -14,6 +14,14 @@ const deviceFactsKey = 'deviceFacts';
const subnetsKey = 'subnets';
const settingsKey = 'settings';
+const encodeToBase64 = (obj) => {
+ return Buffer.from(JSON.stringify(obj)).toString('base64');
+};
+
+const decodeFromBase64 = (str) => {
+ return JSON.parse(Buffer.from(str, 'base64').toString());
+};
+
// Function to get the session
const getSession = async () => {
let session = await db.findOne({ _id: sessionKey });
@@ -131,22 +139,30 @@ export const msGetTheme = async () => {
export const msSetCloudOrgs = async (orgs) => {
const session = await getSession();
- session.cloudOrgs = orgs;
+ const encodedOrgs = encodeToBase64(orgs);
+ session.cloudOrgs = encodedOrgs;
await db.update({ _id: sessionKey }, session);
};
export const msGetCloudOrgs = async () => {
const session = await getSession();
- return session.cloudOrgs;
+ if (session.cloudOrgs && typeof session.cloudOrgs === 'string') {
+ return decodeFromBase64(session.cloudOrgs);
+ }
+ return {};
};
export const msSetCloudInventory = async (inventory) => {
- await db.update({ _id: cloudInventoryKey }, { _id: cloudInventoryKey, data: inventory }, { upsert: true });
+ const encodedInventory = encodeToBase64(inventory);
+ await db.update({ _id: cloudInventoryKey }, { _id: cloudInventoryKey, data: encodedInventory }, { upsert: true });
};
export const msGetCloudInventory = async () => {
const doc = await db.findOne({ _id: cloudInventoryKey });
- return doc ? doc.data : [];
+ if (doc && typeof doc.data === 'string') {
+ return decodeFromBase64(doc.data);
+ }
+ return [];
};
export const msGetOrgFilter = async () => {
@@ -169,14 +185,6 @@ export const msGetLocalInventory = async () => {
return doc ? doc.data : [];
};
-const encodeToBase64 = (obj) => {
- return Buffer.from(JSON.stringify(obj)).toString('base64');
-};
-
-const decodeFromBase64 = (str) => {
- return JSON.parse(Buffer.from(str, 'base64').toString());
-};
-
export const msSaveDeviceFacts = async (facts) => {
const encodedFacts = encodeToBase64(facts);
await db.update({ _id: deviceFactsKey }, { _id: deviceFactsKey, data: encodedFacts }, { upsert: true });