diff --git a/package.json b/package.json index 923f6b5..1796c7a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "webex-v4-toolbox-api", - "version": "2021.9.1", + "version": "2021.11.2-1", "description": "REST API for the dCloud Webex Contact Center v4 instant demo web portal", "main": "src/index.js", "scripts": { @@ -24,6 +24,7 @@ "dotenv": "^8.2.0", "fast-xml-parser": "^3.18.0", "moment": "^2.29.1", + "node-cache": "^5.1.2", "node-fetch": "^2.6.0", "simple-ldap-client": "^2020.12.15", "webex-control-hub": "^1.8.0", diff --git a/src/models/deprovision/index.js b/src/models/deprovision/index.js index bd71607..c943d11 100644 --- a/src/models/deprovision/index.js +++ b/src/models/deprovision/index.js @@ -1,70 +1,70 @@ require('dotenv').config() const ch = require('../control-hub/client') -// const cjpClient = require('../cjp/client') +const cjpClient = require('../cjp/client') const toolbox = require('../toolbox') const teamsNotifier = require('../teams-notifier') // wrapper to translate the `await cjp.get()` call -// const cjp = { -// async get () { -// return cjpClient -// } -// } +const cjp = { + async get () { + return cjpClient + } +} // const fetch = require('../fetch') -// const globals = require('../globals') +const globals = require('../globals') // const https = require('https') // const makeJwt = require('../make-jwt') -// const routingStrategy = require('./routing-strategy') +const routingStrategy = require('./routing-strategy') // const ldap = require('../ldap') // delete team -// async function deleteTeam (name) { -// try { -// const client = await cjp.get() -// const teams = await client.team.list() -// const teamsToDelete = teams.auxiliaryDataList.filter(v => { -// return v.attributes.name__s === name -// }) -// for (const team of teamsToDelete) { -// try { -// await client.team.delete(team.id) -// console.log(`successfully deleted user team ${team.attributes.name__s} (${team.id})`) -// } catch (e) { -// console.log(`failed to delete user team ${team.attributes.name__s} (${team.id}): ${e.message}`) -// } -// } -// } catch (e) { -// throw e -// } -// } +async function deleteTeam (name) { + try { + const client = await cjp.get() + const teams = await client.team.list() + const teamsToDelete = teams.auxiliaryDataList.filter(v => { + return v.attributes.name__s === name + }) + for (const team of teamsToDelete) { + try { + await client.team.delete(team.id) + console.log(`successfully deleted user team ${team.attributes.name__s} (${team.id})`) + } catch (e) { + console.log(`failed to delete user team ${team.attributes.name__s} (${team.id}): ${e.message}`) + } + } + } catch (e) { + throw e + } +} // get virtual teams (queues and entry points) -// async function deleteVirtualTeam (name) { -// const client = await cjp.get() -// let queuesToDelete -// // find the queue first -// try { -// const queues = await client.virtualTeam.list() -// queuesToDelete = queues.auxiliaryDataList.filter(v => { -// return v.attributes.name__s === name -// }) -// } catch (e) { -// console.log(`no virtual team named "${name}" found to delete.`) -// return -// } -// // found? -// if (queuesToDelete.length) { -// for (const queue of queuesToDelete) { -// console.log(`deleting virtual team ${queue.attributes.name__s} (${queue.id})...`) -// try { -// await client.virtualTeam.delete(queue.id) -// console.log(`virtual team ${queue.attributes.name__s} (${queue.id}) deleted.`) -// } catch (e) { -// console.log(`failed to delete virtual team ${queue.attributes.name__s} (${queue.id})`) -// throw e -// } -// } -// } -// } +async function deleteVirtualTeam (name) { + const client = await cjp.get() + let queuesToDelete + // find the queue first + try { + const queues = await client.virtualTeam.list() + queuesToDelete = queues.auxiliaryDataList.filter(v => { + return v.attributes.name__s === name + }) + } catch (e) { + console.log(`no virtual team named "${name}" found to delete.`) + return + } + // found? + if (queuesToDelete.length) { + for (const queue of queuesToDelete) { + console.log(`deleting virtual team ${queue.attributes.name__s} (${queue.id})...`) + try { + await client.virtualTeam.delete(queue.id) + console.log(`virtual team ${queue.attributes.name__s} (${queue.id}) deleted.`) + } catch (e) { + console.log(`failed to delete virtual team ${queue.attributes.name__s} (${queue.id})`) + throw e + } + } + } +} // //Get the Chat Template ID needed for Cumulus Chat routing // async function findTemplates (name) { @@ -239,49 +239,49 @@ async function removeRoles (userId) { } // remove team from global voice queue distribution group -// async function removeVoiceQueueTeam (teamName) { -// try { -// await Promise.resolve(globals.initialLoad) -// const queueName = globals.get('webexV4VoiceQueueName') -// const client = await cjp.get() -// const teams = await client.team.list() -// const team = teams.auxiliaryDataList.find(v => v.attributes.name__s === teamName) -// if (!team) { -// // throw Error(`team "${teamName}" not found`) -// return -// } -// const queues = await client.virtualTeam.list() -// const queue = queues.auxiliaryDataList.find(v => v.attributes.name__s === queueName) -// if (!queue) { -// throw Error(`queue "${queueName}" not found`) -// } -// // fix attributes from GET data for using in PUT operation -// queue.attributes.tid__s = queue.attributes.tid -// queue.attributes.sid__s = queue.attributes.sid -// queue.attributes.cstts__l = queue.attributes.cstts +async function removeVoiceQueueTeam (teamName) { + try { + await Promise.resolve(globals.initialLoad) + const queueName = globals.get('webexV4VoiceQueueName') + const client = await cjp.get() + const teams = await client.team.list() + const team = teams.auxiliaryDataList.find(v => v.attributes.name__s === teamName) + if (!team) { + // throw Error(`team "${teamName}" not found`) + return + } + const queues = await client.virtualTeam.list() + const queue = queues.auxiliaryDataList.find(v => v.attributes.name__s === queueName) + if (!queue) { + throw Error(`queue "${queueName}" not found`) + } + // fix attributes from GET data for using in PUT operation + queue.attributes.tid__s = queue.attributes.tid + queue.attributes.sid__s = queue.attributes.sid + queue.attributes.cstts__l = queue.attributes.cstts -// delete queue.attributes.tid -// delete queue.attributes.sid -// delete queue.attributes.cstts - -// // get existing call distribution groups -// const groups = JSON.parse(queue.attributes.callDistributionGroups__s) -// // get the first distribution group -// const group = groups.find(v => v.order === 1) -// if (!group) { -// throw Error(`call distribution group 1 not found`) -// } -// // filter out the agent groups matching the team ID -// group.agentGroups = group.agentGroups.filter(v => v.teamId !== team.id) -// queue.attributes.callDistributionGroups__s = JSON.stringify(groups) -// // update queue on CJP -// await client.virtualTeam.modify(queue.id, [queue]) -// console.log(`successfully removed team "${teamName}" (${team.id}) from the global voice queue "${queue.attributes.name__s}" (${queue.id})`) -// } catch (e) { -// // console.log(`failed to remove team "${teamName}" from the global voice queue:`, e.message) -// throw e -// } -// } + delete queue.attributes.tid + delete queue.attributes.sid + delete queue.attributes.cstts + + // get existing call distribution groups + const groups = JSON.parse(queue.attributes.callDistributionGroups__s) + // get the first distribution group + const group = groups.find(v => v.order === 1) + if (!group) { + throw Error(`call distribution group 1 not found`) + } + // filter out the agent groups matching the team ID + group.agentGroups = group.agentGroups.filter(v => v.teamId !== team.id) + queue.attributes.callDistributionGroups__s = JSON.stringify(groups) + // update queue on CJP + await client.virtualTeam.modify(queue.id, [queue]) + console.log(`successfully removed team "${teamName}" (${team.id}) from the global voice queue "${queue.attributes.name__s}" (${queue.id})`) + } catch (e) { + // console.log(`failed to remove team "${teamName}" from the global voice queue:`, e.message) + throw e + } +} async function main (user) { if (!user.id || !user.id.length === 4) { @@ -291,58 +291,58 @@ async function main (user) { try { console.log(`deprovisioning user ${userId}...`) // chat queue - // try { - // console.log(`checking chat queues...`) - // await deleteVirtualTeam(`Q_Chat_dCloud_${userId}`) - // } catch (e) { - // console.log(`failed to delete virtual team Q_Chat_dCloud_${userId}:`, e.message) - // throw e - // } + try { + console.log(`checking chat queues...`) + await deleteVirtualTeam(`Q_Chat_dCloud_${userId}`) + } catch (e) { + console.log(`failed to delete virtual team Q_Chat_dCloud_${userId}:`, e.message) + throw e + } // voice queue - // try { - // console.log(`checking voice queues...`) - // await deleteVirtualTeam(`Q_dCloud_${userId}`) - // } catch (e) { - // console.log(`failed to delete virtual team Q_dCloud_${userId}:`, e.message) - // throw e - // } + try { + console.log(`checking voice queues...`) + await deleteVirtualTeam(`Q_dCloud_${userId}`) + } catch (e) { + console.log(`failed to delete virtual team Q_dCloud_${userId}:`, e.message) + throw e + } // remove team from global voice queue - // try { - // console.log(`checking global voice queue distribution groups...`) - // await removeVoiceQueueTeam(`T_dCloud_${userId}`) - // } catch (e) { - // console.log(`failed to remove team T_dCloud_${userId} from global voice queue distribution groups:`, e.message) - // throw e - // } + try { + console.log(`checking global voice queue distribution groups...`) + await removeVoiceQueueTeam(`T_dCloud_${userId}`) + } catch (e) { + console.log(`failed to remove team T_dCloud_${userId} from global voice queue distribution groups:`, e.message) + throw e + } // email queue - // try { - // console.log(`checking email queues...`) - // await deleteVirtualTeam(`Q_Email_dCloud_${userId}`) - // } catch (e) { - // console.log(`failed to delete virtual team Q_Email_dCloud_${userId}:`, e.message) - // throw e - // } + try { + console.log(`checking email queues...`) + await deleteVirtualTeam(`Q_Email_dCloud_${userId}`) + } catch (e) { + console.log(`failed to delete virtual team Q_Email_dCloud_${userId}:`, e.message) + throw e + } // chat entry point - // try { - // console.log(`checking chat entry points...`) - // await deleteVirtualTeam(`EP_Chat_${userId}`) - // } catch (e) { - // console.log(`failed to delete virtual team EP_Chat_${userId}:`, e.message) - // throw e - // } + try { + console.log(`checking chat entry points...`) + await deleteVirtualTeam(`EP_Chat_${userId}`) + } catch (e) { + console.log(`failed to delete virtual team EP_Chat_${userId}:`, e.message) + throw e + } // user team - // try { - // console.log(`checking teams...`) - // await deleteTeam(`T_dCloud_${userId}`) - // } catch (e) { - // console.log(`failed to delete team T_dCloud_${userId}:`, e.message) - // throw e - // } + try { + console.log(`checking teams...`) + await deleteTeam(`T_dCloud_${userId}`) + } catch (e) { + console.log(`failed to delete team T_dCloud_${userId}:`, e.message) + throw e + } // Chat Template @@ -353,110 +353,109 @@ async function main (user) { // console.log('failed to delete chat template:', e.message) // throw e // } - // Routing Strategies // chat queue - // try { - // console.log(`checking chat queue routing strategies...`) - // await routingStrategy.delete(`RS_Chat_${userId}`) - // } catch (e) { - // console.log(`failed to delete routing strategy RS_Chat_${userId}:`, e.message) - // throw e - // } + try { + console.log(`checking chat queue routing strategies...`) + await routingStrategy.delete(`RS_Chat_${userId}`) + } catch (e) { + console.log(`failed to delete routing strategy RS_Chat_${userId}:`, e.message) + throw e + } - // try { - // console.log(`checking chat queue current routing strategies...`) - // await routingStrategy.delete(`Current-RS_Chat_${userId}`) - // } catch (e) { - // console.log(`failed to delete routing strategy Current-RS_EP_Chat_${userId}:`, e.message) - // throw e - // } + try { + console.log(`checking chat queue current routing strategies...`) + await routingStrategy.delete(`Current-RS_Chat_${userId}`) + } catch (e) { + console.log(`failed to delete routing strategy Current-RS_EP_Chat_${userId}:`, e.message) + throw e + } // chat queue again - // try { - // console.log(`checking another format of chat queue routing strategies...`) - // await routingStrategy.delete(`RS_Chat_dCloud_${userId}`) - // } catch (e) { - // console.log(`failed to delete routing strategy RS_Chat_dCloud_${userId}:`, e.message) - // throw e - // } + try { + console.log(`checking another format of chat queue routing strategies...`) + await routingStrategy.delete(`RS_Chat_dCloud_${userId}`) + } catch (e) { + console.log(`failed to delete routing strategy RS_Chat_dCloud_${userId}:`, e.message) + throw e + } - // try { - // console.log(`checking another format of chat queue current routing strategies...`) - // await routingStrategy.delete(`Current-RS_Chat_dCloud_${userId}`) - // } catch (e) { - // console.log(`failed to delete routing strategy Current-RS_Chat_dCloud_${userId}:`, e.message) - // throw e - // } + try { + console.log(`checking another format of chat queue current routing strategies...`) + await routingStrategy.delete(`Current-RS_Chat_dCloud_${userId}`) + } catch (e) { + console.log(`failed to delete routing strategy Current-RS_Chat_dCloud_${userId}:`, e.message) + throw e + } // chat entry point - // try { - // console.log(`checking chat entry point routing strategies...`) - // await routingStrategy.delete(`RS_EP_Chat_${userId}`) - // } catch (e) { - // console.log(`failed to delete routing strategy RS_EP_Chat_${userId}:`, e.message) - // throw e - // } + try { + console.log(`checking chat entry point routing strategies...`) + await routingStrategy.delete(`RS_EP_Chat_${userId}`) + } catch (e) { + console.log(`failed to delete routing strategy RS_EP_Chat_${userId}:`, e.message) + throw e + } - // try { - // console.log(`checking chat entry point current routing strategies...`) - // await routingStrategy.delete(`Current-RS_EP_Chat_${userId}`) - // } catch (e) { - // console.log(`failed to delete routing strategy Current-RS_EP_Chat_${userId}:`, e.message) - // throw e - // } + try { + console.log(`checking chat entry point current routing strategies...`) + await routingStrategy.delete(`Current-RS_EP_Chat_${userId}`) + } catch (e) { + console.log(`failed to delete routing strategy Current-RS_EP_Chat_${userId}:`, e.message) + throw e + } // chat entry point again - // try { - // console.log(`checking another format of chat entry point routing strategies...`) - // await routingStrategy.delete(`EP_Chat_${userId}`) - // } catch (e) { - // console.log(`failed to delete routing strategy EP_Chat_${userId}:`, e.message) - // throw e - // } + try { + console.log(`checking another format of chat entry point routing strategies...`) + await routingStrategy.delete(`EP_Chat_${userId}`) + } catch (e) { + console.log(`failed to delete routing strategy EP_Chat_${userId}:`, e.message) + throw e + } - // try { - // console.log(`checking another format of chat entry point current routing strategies...`) - // await routingStrategy.delete(`Current-EP_Chat_${userId}`) - // } catch (e) { - // console.log(`failed to delete routing strategy Current-EP_Chat_${userId}:`, e.message) - // throw e - // } + try { + console.log(`checking another format of chat entry point current routing strategies...`) + await routingStrategy.delete(`Current-EP_Chat_${userId}`) + } catch (e) { + console.log(`failed to delete routing strategy Current-EP_Chat_${userId}:`, e.message) + throw e + } // email routing strategy - // try { - // console.log(`checking email queue routing strategies...`) - // await routingStrategy.delete(`RS_Email_dCloud_${userId}`) - // } catch (e) { - // console.log(`failed to delete routing strategy RS_Email_dCloud_${userId}:`, e.message) - // throw e - // } + try { + console.log(`checking email queue routing strategies...`) + await routingStrategy.delete(`RS_Email_dCloud_${userId}`) + } catch (e) { + console.log(`failed to delete routing strategy RS_Email_dCloud_${userId}:`, e.message) + throw e + } - // try { - // console.log(`checking email queue current routing strategies...`) - // await routingStrategy.delete(`Current-RS_Email_dCloud_${userId}`) - // } catch (e) { - // console.log(`failed to delete routing strategy Current-RS_Email_dCloud_${userId}:`, e.message) - // throw e - // } + try { + console.log(`checking email queue current routing strategies...`) + await routingStrategy.delete(`Current-RS_Email_dCloud_${userId}`) + } catch (e) { + console.log(`failed to delete routing strategy Current-RS_Email_dCloud_${userId}:`, e.message) + throw e + } // voice queue - // try { - // console.log(`checking voice queue routing strategies...`) - // await routingStrategy.delete(`RS_dCloud_${userId}`) - // } catch (e) { - // console.log(`failed to delete routing strategy RS_dCloud_${userId}:`, e.message) - // throw e - // } + try { + console.log(`checking voice queue routing strategies...`) + await routingStrategy.delete(`RS_dCloud_${userId}`) + } catch (e) { + console.log(`failed to delete routing strategy RS_dCloud_${userId}:`, e.message) + throw e + } - // try { - // console.log(`checking voice queue current routing strategies...`) - // await routingStrategy.delete(`Current-RS_dCloud_${userId}`) - // } catch (e) { - // console.log(`failed to delete routing strategy Current-RS_dCloud_${userId}:`, e.message) - // throw e - // } + try { + console.log(`checking voice queue current routing strategies...`) + await routingStrategy.delete(`Current-RS_dCloud_${userId}`) + } catch (e) { + console.log(`failed to delete routing strategy Current-RS_dCloud_${userId}:`, e.message) + throw e + } // Skill Profiles // try { @@ -477,13 +476,13 @@ async function main (user) { // } // email routing strategy queue - // try { - // console.log(`checking global email routing strategy...`) - // await routingStrategy.globalEmail.delete(`Q_Email_dCloud_${userId}`) - // } catch (e) { - // console.log(`failed to delete queue Q_Email_dCloud_${userId} from the global email routing strategy:`, e.message) - // throw e - // } + try { + console.log(`checking global email routing strategy...`) + await routingStrategy.globalEmail.delete(`Q_Email_dCloud_${userId}`) + } catch (e) { + console.log(`failed to delete queue Q_Email_dCloud_${userId} from the global email routing strategy:`, e.message) + throw e + } // delete LDAP users // try { @@ -515,13 +514,16 @@ async function main (user) { // remove provision info from database try { console.log(`setting user provision info to not provisioned for webex-v4prod...`) + // don't change the user's last access time when updating their provision info + const ignoreAccessTime = true await toolbox.updateUser(userId, { provision: 'deleted', + deprovisionDate: new Date().toJSON(), password: null, error: null - }) + }, ignoreAccessTime) console.log(`successfully set user provision info to not provisioned for webex-v4prod`) - teamsNotifier.deprovision(user) + // teamsNotifier.deprovision(user) } catch (e) { console.log(`failed to set user provision info to not provisioned for webex-v4prod:`, e.message) throw e diff --git a/src/models/provision.js b/src/models/provision.js index 10c7e7b..4c02ce0 100644 --- a/src/models/provision.js +++ b/src/models/provision.js @@ -204,8 +204,14 @@ module.exports = async function (user) { typeName: 'chat entry point' }) + // generate a random hour of the day for script start. this fixes + // CCBU db query/logging issue when many current routing strategies are + // generated at the same time + const randomHour = Math.floor(Math.random() * 24) + const randomTime = randomHour * 60 * 60 * 1000 + // chat entry point routing strategy - await provision({ + const parentStrategy = await provision({ templateName: chatEntryPointRoutingStrategyTemplateName, name: 'EP_Chat_' + userId, type: 'routingStrategy', @@ -220,6 +226,12 @@ module.exports = async function (user) { json['call-distribution-script']['@_scriptid'] = now // start date is start of day today in milliseconds json['call-distribution-script']['@_start-date'] = startOfToday + // set the start time and end time to the same random time + body.attributes.startTime__l = randomTime + body.attributes.endTime__l = randomTime + // and set start and end time in the script data + json['call-distribution-script']['@_execution-start-time-of-day'] = String(randomTime) + json['call-distribution-script']['@_execution-end-time-of-day'] = String(randomTime) // chat entry point ID json['call-distribution-script']['vdn']['@_id'] = chatEntryPoint.attributes.dbId__l // chat entry point db ID @@ -254,6 +266,12 @@ module.exports = async function (user) { const startOfToday = Math.floor(now / 1000 / 86400) * 86400 * 1000 json['call-distribution-script']['@_start-date'] = startOfToday json['call-distribution-script']['@_end-date'] = startOfToday + // set the start time and end time to the same random time + body.attributes.startTime__l = randomTime + body.attributes.endTime__l = randomTime + // and set start and end time in the script data + json['call-distribution-script']['@_execution-start-time-of-day'] = String(randomTime) + json['call-distribution-script']['@_execution-end-time-of-day'] = String(randomTime) // chat entry point ID json['call-distribution-script']['vdn']['@_id'] = chatEntryPoint.attributes.dbId__l // chat entry point db ID @@ -268,6 +286,8 @@ module.exports = async function (user) { body.attributes.legacyVirtualTeamId__l = chatEntryPoint.attributes.dbId__l // chat entry point ID body.attributes.virtualTeamId__s = chatEntryPoint.id + // set parent RS + body.attributes.parentStrategyId__s = parentStrategy.id } }) @@ -291,7 +311,6 @@ module.exports = async function (user) { }) // add read-only admin role to Rick user in Webex Control Hub - ch.user.modify({ userId: chRick.id, roles: ['id_readonly_admin'] @@ -519,6 +538,7 @@ module.exports = async function (user) { // and remove any previous errors await toolbox.updateUser(userId, { provision: 'complete', + provisionDate: new Date().toJSON(), password: null, error: null }) diff --git a/src/models/schedule.js b/src/models/schedule.js index 6bc6800..53ba738 100644 --- a/src/models/schedule.js +++ b/src/models/schedule.js @@ -7,13 +7,19 @@ const globals = require('./globals') const teamsLogger = require('./teams-logger') const ldap = require('./ldap') const teamsNotifier = require('./teams-notifier') +const NodeCache = require('node-cache') + +// caching for file GET requests +const cache = new NodeCache({ + // keep in cache forever + stdTTL: 0, + // create copies of data + useClones: true +}) // number of milliseconds to wait after completing the scheduled job before // starting again -const throttle = 10 * 1000 - -// running state -let running = false +const throttle = 40 * 1000 async function getProvisionStartedUsers () { // return array of users with a user ID who need to be provisioned @@ -65,28 +71,31 @@ async function getProvisionDeletingUsers () { // so they will be deprovisioned async function checkMaxUsers () { try { + console.log('checking max users using Control Hub...') // wait for globals to exist + console.log('waiting for global variables to exist...') await Promise.resolve(globals.initialLoad) + console.log('global variables are set.') // max number of users that can be provisioned. more than this will trigger // deprovision. this is number of toolbox users. const maxUsers = parseInt(globals.get('webexV4MaxUsers')) + console.log('global variables webexV4MaxUsers is', maxUsers) // number of users to delete below the maxUser limit const maxUsersBuffer = parseInt(globals.get('webexV4MaxUsersBuffer')) + console.log('global variables webexV4MaxUsersBuffer is', maxUsersBuffer) // find license usage in control hub const client = await ch.getClient() - const licenseUsage = await client.org.getLicenseUsage() - const cjpPremiumLicenses = licenseUsage[0].licenses.find(v => v.offerName === 'CJPPRM') - // if license usage of CJP premium is > 95% - // if (cjpPremiumLicenses.usage / cjpPremiumLicenses.volume >= 0.95) { - // if (cjpPremiumLicenses.volume - cjpPremiumLicenses.usage <= 10) { - // console.log('cjpPremiumLicenses.usage', cjpPremiumLicenses.usage) - // console.log('maxUsers', maxUsers) + const licenseUsage = await getLicenseUsageCount() // delete 10 users below the max, so that old users can be cycled out when // new users are waiting to be provisioned and we are at max capacity - if (cjpPremiumLicenses.usage / 2 > maxUsers - maxUsersBuffer) { + if (licenseUsage / 2 > maxUsers - maxUsersBuffer) { // too full - need to deprovision some users + // const qtyUsersToDeprovision = cjpPremiumLicenses.usage / 2 - maxUsers + maxUsersBuffer + // console.log('max users has been reached. we need to mark', qtyUsersToDeprovision, 'for deprovision.') // get all control hub users + console.log('getting all users list from cjp...') const allUsers = await client.user.listAll() + console.log('got all users list from cjp:', allUsers.length, 'users.') // filter control hub users that do not have CJPPRM license const licensedUsers = allUsers.filter(user => { try { @@ -98,6 +107,7 @@ async function checkMaxUsers () { return false } }) + console.log('licensedUsers count =', licensedUsers.length) // find user provision info for this demo const query = { 'demo.webex-v4prod.lastAccess': {$exists: 1}, @@ -106,11 +116,12 @@ async function checkMaxUsers () { // const projection = {password: false} const projection = {id: 1, 'demo.webex-v4prod.lastAccess': 1} const provisionedUsers = await toolbox.findUsers(query, projection) - // console.log('provisionedUsers', provisionedUsers) + console.log('found', provisionedUsers.length, 'users with complete webex provision and a last access time in the toolbox') // filter provisioned toolbox users to those with matching licensed control hub users const userMap = provisionedUsers.filter(user => { return licensedUsers.find(agent => user.id === agent.userName.slice(8, 12)) }) + console.log(userMap.length, 'users in userMap') // sort by last access time descending userMap.sort((a, b) => { const aDate = new Date(a.demo['webex-v4prod'].lastAccess || 0) @@ -121,10 +132,17 @@ async function checkMaxUsers () { // console.log('userMap', userMap) // keep top users, return the rest // return userMap.slice(maxUsers) - // set each of these users to delete state - const userIds = userMap.slice(maxUsers - maxUsersBuffer).map(v => v.id) - // console.log('user IDs to deprovision:', userIds) + const qtyUsersToDeprovision = licensedUsers.length / 2 - maxUsers + maxUsersBuffer + // only continue if quantity to deprovision is 1 or more + if (qtyUsersToDeprovision < 1) { + return + } + console.log('selecting', qtyUsersToDeprovision, 'users to deprovision...') + // select the quantity of users to delete from the end of the list (users with last access) + const userIds = userMap.slice(-1 * qtyUsersToDeprovision).map(v => v.id) + console.log('user IDs to deprovision:', userIds) if (userIds.length === 0) { + console.log('no users to deprovision at this time.') // no users to deprovision. done. return } @@ -133,7 +151,7 @@ async function checkMaxUsers () { // log to webex staff room teamsNotifier.markDeprovision(userIds) // update toolbox database - return toolbox.updateDemoUsers(filter, updates) + toolbox.updateDemoUsers(filter, updates) } else { // not full - return empty array // return [] @@ -143,125 +161,160 @@ async function checkMaxUsers () { } } -// find license usage in control hub +async function updateLicenseUsageCache () { + try { + const client = await ch.getClient() + cache.set('licenseUsage', await client.org.getLicenseUsage()) + } catch (e) { + throw e + } +} + +// find CJPPRM license usage in control hub async function getLicenseUsageCount () { - const client = await ch.getClient() - const licenseUsage = await client.org.getLicenseUsage() - const cjpPremiumLicenses = licenseUsage[0].licenses.find(v => v.offerName === 'CJPPRM') - return cjpPremiumLicenses.usage + try { + // check cache + let licenseUsage = cache.get('licenseUsage') + // if cache miss + if (!licenseUsage) { + // update cache + await updateLicenseUsageCache() + // get cache again + licenseUsage = cache.get('licenseUsage') + } + console.log(licenseUsage) + // return cjpprm usage value from cache + const cjpPremiumLicenses = licenseUsage[0].licenses.find(v => v.offerName === 'CJPPRM') + return cjpPremiumLicenses.usage + } catch (e) { + throw e + } } async function go () { - // don't do anything if provisioning is already in progress - if (!running) { - running = true - // check if there are too many users provisioned - try { - await checkMaxUsers() - } catch (e) { - console.log('failed to check max users:', e) + // check if provisioning enabled + const enabledString = await globals.getAsync('webexV4ProvisionEnabled') + const enabled = enabledString === 'true' + if (!enabled) { + // do nothing now - set timer to call this function again + setTimeout(go, throttle) + return + } + + // check if there are too many users provisioned + try { + await checkMaxUsers() + } catch (e) { + console.log('failed to check max users:', e) + } + console.log('done checking max users.') + // get list of users to deprovision + try { + console.log('getProvisionDeletingUsers...') + const users = await getProvisionDeletingUsers() + console.log(users.length, 'users to deprovision.') + if (users.length > 0) { + console.log(`starting deprovision for ${users.length} users`) } - // get list of users to deprovision - try { - const users = await getProvisionDeletingUsers() - if (users.length > 0) { - console.log(`starting deprovision for ${users.length} users`) - } - for (const user of users) { - await deprovision(user) - } - } catch (e) { - console.log('deprovision error:', e.message) + for (const user of users) { + await deprovision(user) } - // get list of users to provision - try { - // wait for globals to exist - let users = await getProvisionStartedUsers() - // get max users number - await Promise.resolve(globals.initialLoad) - const maxUsers = parseInt(globals.get('webexV4MaxUsers')) - const licenseUsageCount = await getLicenseUsageCount() - // check if provision amount would be too many - if (licenseUsageCount / 2 + users.length > maxUsers) { - // trim? - const max = Math.floor(maxUsers - (licenseUsageCount / 2)) - users = users.slice(0, max) - } - if (users.length > 0) { - console.log(`starting provision for ${users.length} users`) - const errorUsers = [] - // provision LDAP users - for (const user of users) { - // create rbarrowsXXXX, sjeffersXXXX, and VPN LDAP accounts - try { - await ldap.createUsers({user}) - } catch (e) { - console.log('ldap.createUsers error:', e.message) - // error from LDAP that the new user's VPN password is not valid - // (too short, etc.) - const ldapPasswordError = /DSID-031A12D2/ - console.log('ldapPasswordError.test(e.message)', ldapPasswordError.test(e.message)) - if (ldapPasswordError.test(e.message)) { - console.log('user password is invalid. updating user provision with error...') - // user's password is not valid for LDAP to set their VPN user - // password update user provision data so toolbox can notify user - const updates = { - provision: 'error', - error: 'Invalid password. Please provision using a VPN password that is 10 or more characters.', - } - // update the user with the error - toolbox.updateUser(user.id, updates) - .then(r => { - console.log('updated user', user.id, 'with invalid password provision error.') - }).catch(e2 => { - console.log('failed to update user', user.id, 'with invalid password provision error:', e2.message) - }) - // mark user should not be provisioned in CJP and control hub - errorUsers.push(user.id) - // continue with next user - continue + } catch (e) { + console.log('deprovision error:', e.message) + } + console.log('getProvisionDeletingUsers done.') + // get list of users to provision + try { + // wait for globals to exist + console.log('getProvisionStartedUsers...') + let users = await getProvisionStartedUsers() + console.log(users.length, 'users need to be provisioned.') + // get max users number + await Promise.resolve(globals.initialLoad) + console.log('global variables are loaded.') + const maxUsers = parseInt(globals.get('webexV4MaxUsers'), 10) + const licenseUsageCount = await getLicenseUsageCount() + console.log('licenseUsageCount =', licenseUsageCount) + // check if provision amount would be too many + if (licenseUsageCount / 2 + users.length > maxUsers) { + console.log('current license users count', licenseUsageCount / 2 + users.length, 'is greater than max users value of', maxUsers) + // trim? + const max = Math.floor(maxUsers - (licenseUsageCount / 2)) + users = users.slice(0, max) + } + if (users.length > 0) { + console.log(`starting provision for ${users.length} users`) + const errorUsers = [] + // provision LDAP users + for (const user of users) { + // create rbarrowsXXXX, sjeffersXXXX, and VPN LDAP accounts + try { + await ldap.createUsers({user}) + } catch (e) { + console.log('ldap.createUsers error:', e.message) + // error from LDAP that the new user's VPN password is not valid + // (too short, etc.) + const ldapPasswordError = /DSID-031A12D2/ + console.log('ldapPasswordError.test(e.message)', ldapPasswordError.test(e.message)) + if (ldapPasswordError.test(e.message)) { + console.log('user password is invalid. updating user provision with error...') + // user's password is not valid for LDAP to set their VPN user + // password update user provision data so toolbox can notify user + const updates = { + provision: 'error', + error: 'Invalid password. Please provision using a VPN password that is 10 or more characters.', } + // update the user with the error + toolbox.updateUser(user.id, updates) + .then(r => { + console.log('updated user', user.id, 'with invalid password provision error.') + }).catch(e2 => { + console.log('failed to update user', user.id, 'with invalid password provision error:', e2.message) + }) + // mark user should not be provisioned in CJP and control hub + errorUsers.push(user.id) + // continue with next user + continue } } - // provision the rest of the user stuff in CJP and control hub? - if (process.env.PROVISION_ALL === 'true') { - // filter out any users who had an error during LDAP provisioning - const successfulUsers = users.filter(v => !errorUsers.includes(v.id)) - for (const user of successfulUsers) { - await provision(user) - } + } + // provision the rest of the user stuff in CJP and control hub? + if (process.env.PROVISION_ALL === 'true') { + // filter out any users who had an error during LDAP provisioning + const successfulUsers = users.filter(v => !errorUsers.includes(v.id)) + for (const user of successfulUsers) { + await provision(user) } - } else { - // no users to provision + // update the license usage count + updateLicenseUsageCache() } - } catch (e) { - const s = e.message - const message = `provision error: ${s}` + } else { + // no users to provision + } + } catch (e) { + const s = e.message + const message = `provision error: ${s}` + console.log(message) + // outgoing network error message (probably from Atlas) + const generalNetworkError = /getaddrinfo ENOTFOUND|getaddrinfo EAI_AGAIN|connect ETIMEDOUT|read ECONNRESET|504 Gateway Time-out|502 Bad Gateway/ + if (generalNetworkError.test(message)) { + // just log to console + // atlas-a.wbx2.com and dcloud-collab-toolbox.cxdemo.net give these + // errors often and just need to retry in a moment console.log(message) - // outgoing network error message (probably from Atlas) - const generalNetworkError = /getaddrinfo ENOTFOUND|getaddrinfo EAI_AGAIN|connect ETIMEDOUT|read ECONNRESET|504 Gateway Time-out|502 Bad Gateway/ - if (generalNetworkError.test(message)) { - // just log to console - // atlas-a.wbx2.com and dcloud-collab-toolbox.cxdemo.net give these - // errors often and just need to retry in a moment - console.log(message) - // retry now - running = false - go() - return - } else { - // send any other, unexpected errors to teams logger - teamsLogger.log(message) - } + } else { + // send any other, unexpected errors to teams logger + teamsLogger.log(message) } - // stop running - running = false } + console.log('provision check complete. next check in', Math.round(throttle / 1000), 'seconds.') + // set timer to call this function again + setTimeout(go, throttle) } go() -setInterval(go, throttle) +// setInterval(go, throttle) module.exports = { getProvisionDeletingUsers diff --git a/src/models/toolbox.js b/src/models/toolbox.js index ab7b50d..588deec 100644 --- a/src/models/toolbox.js +++ b/src/models/toolbox.js @@ -3,8 +3,8 @@ const fetch = require('./fetch') const urlBase = 'https://dcloud-collab-toolbox.cxdemo.net/api/v1/auth' // const urlBase = 'http://localhost:3032/api/v1/auth' -async function updateUser (userId, body) { - return updateDemoUsers({id: userId}, body) +async function updateUser (userId, body, ignoreAccessTime) { + return updateDemoUsers({id: userId}, body, ignoreAccessTime) } async function findUsers (query = {}, projection = {}) { @@ -26,7 +26,7 @@ async function findUsers (query = {}, projection = {}) { } } -async function updateDemoUsers (filter, updates) { +async function updateDemoUsers (filter, updates, ignoreAccessTime = false) { try { const url = urlBase + '/app/demo/webex-v4prod/users' const options = { @@ -37,6 +37,9 @@ async function updateDemoUsers (filter, updates) { body: { filter, updates + }, + query: { + ignoreAccessTime } } return fetch(url, options) diff --git a/test/random-time.js b/test/random-time.js new file mode 100644 index 0000000..c2b781e --- /dev/null +++ b/test/random-time.js @@ -0,0 +1,6 @@ +// generate a random hour of the day for script start. this fixes +// CCBU db query/logging issue when many current routing strategies are +// generated at the same time +const randomHour = Math.floor(Math.random() * 24) +const randomTime = randomHour * 60 * 60 * 1000 +console.log(String(randomTime)) \ No newline at end of file