From 5d367b6fad395663f92bb0a4734a638db5353e40 Mon Sep 17 00:00:00 2001 From: Simon Rho Date: Tue, 13 Aug 2024 18:58:21 -0400 Subject: [PATCH] Filter by AP type from inventory --- jccm/src/Frontend/Common/CommonVariables.js | 2 +- jccm/src/Frontend/Common/StateStore.js | 4 +- jccm/src/Frontend/Components/Login.js | 6 +- jccm/src/Frontend/Layout/Footer.js | 4 +- .../Frontend/Layout/InventoryTreeMenuLocal.js | 138 ++++++++++++------ jccm/src/Services/ApiCalls.js | 63 ++++++-- jccm/src/Services/ApiServer.js | 51 ++++++- jccm/src/Services/Device.js | 1 + jccm/src/Services/mainStore.js | 32 ++-- 9 files changed, 223 insertions(+), 78 deletions(-) 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 });