Name
sn_cmdb_ws.MultiSourceQueryProcessor
Description
Collection of APIs powering CMDB Multisource workspace dashboard
Script
var MultiSourceQueryProcessor = Class.create();
MultiSourceQueryProcessor.prototype = {
MULTISOURCE_DATA: 'cmdb_multisource_data',
CIS_BY_NUMBER_OF_SOURCES: 'sn_cmdb_ws_ms_cis_by_number_of_sources',
CIS_WITH_SINGLE_SOURCE: 'sn_cmdb_ws_ms_cis_with_single_source',
MULTISOURCE_CLASS_METADATA: 'sn_cmdb_ws_ms_class_metadata',
MULTISOURCE_DASHBOARD_DATA: 'sn_cmdb_ws_ms_ci_dashboard_data',
DISCOVERY_SOURCES_NOT_REPORTING: 'sn_cmdb_ws_ms_discovery_sources_not_reporting',
MULTISOURCE_COLUMN_METADATA: 'cmdb_multisource_column_metadata',
CMDB_MULTISOURCE_DIFF_QUERY_VIEW: 'cmdb_multisource_diff_query_view',
CMDB_MULTISOURCE_DIFF_QUERY_VIEW_PREFIX_1: 'msd1_',
CMDB_MULTISOURCE_DIFF_QUERY_VIEW_PREFIX_2: 'msd2_',
DATA_MISMATCH_CONFIG: 'sn_cmdb_ws_ms_data_mismatch_config',
DATA_MISMATCH: 'sn_cmdb_ws_ms_data_mismatch',
MULTISOURCE_REPORT_MAX_LIMIT_PROPERTY: 'sn_cmdb_ws.ms.report_max_limit',
DISCOVERY_SOURCE_NOT_REPORTING_MAX_DAYS_PROPERTY: 'sn_cmdb_ws.ms.discovery_source_not_reporting_max_days',
GLIDE_AGGREGATES_MAX_LIMIT_PROPERTY: 'glide.db.max.aggregates',
CMDB_CI: 'cmdb_ci',
READ_REPLICA_CATEGORY: 'cmdb_health_score',
// Columns in cmdb_multisource_data
DISCOVERY_SOURCE: 'discovery_source',
CLASS_NAME: 'class',
CI: 'ci',
SYS_ID: 'sys_id',
CMDB_REFERENCE: 'cmdb_reference',
MAX_COUNT: 'max_count',
CURRENT_COUNT: 'current_count',
READ_COUNT: 'read_count',
WRITE_COUNT: 'write_count',
// Columns in sn_cmdb_ws_ms_class_metadata
TABLE: 'table',
CI_COUNT_PERCENT: 'ci_count_percent',
// Columns in sn_cmdb_ws_ms_data_mismatch_config
ATTRIBUTES: 'attributes',
CONDITION_TYPE: 'condition_type',
DISCOVERY_SOURCES: 'discovery_sources',
// Columns in sn_cmdb_ws_ms_data_mismatch
SOURCE_1: 'source_1',
SOURCE_1_VALUE: 'source_1_value',
CLASS_1: 'class_1',
SOURCE_2: 'source_2',
SOURCE_2_VALUE: 'source_2_value',
CLASS_2: 'class_2',
DIFF_FIELD: 'diff_field',
// Columns in sn_cmdb_ws_ms_ci_by_number_source
SOURCE_COUNT: 'source_count',
CI_COUNT: 'ci_count',
STATE: 'state',
DRAFT: 1,
READY: 2,
RETIRED: 3,
//Columns in sn_cmdb_ws_ms_ci_coverage
SOURCE: 'source',
// Columns in table sn_cmdb_ws_ms_ci_dashboard_data
CHART: 'chart',
KEY: 'key',
VALUE: 'value',
// Charts in table sn_cmdb_ws_ms_ci_dashboard_data
NUMBER_OF_DISCOVERY_SOURCES: 'number_of_discovery_sources',
RAW_RECORDS: 'raw_records',
RECONCILED_CIS: 'reconciled_cis',
CHART_DATA_MISMATCH: 'data_mismatch',
CHART_CIS_WITH_SINGLE_SOURCE: 'cis_with_single_source',
CHART_CIS_BY_NUMBER_OF_SOURCES: 'cis_by_number_of_sources',
SOURCE_OVERVIEW: 'source_overview',
CHART_DISCOVERY_SOURCE_NOT_REPORTING: 'discovery_source_not_reporting',
// Use this constant in dashboard data when there are no CIs discovered by single source
ZERO_CIS_WITH_A_SINGLE_SOURCE_KEY: 'zero_cis_with_a_single_source',
ZERO_CIS_WITH_A_SINGLE_SOURCE_VALUE: 0,
// Columns in sn_cmdb_ws_ms_discovery_sources_not_reporting
LAST_DISCOVERED: 'last_discovered',
// Columns in cmdb_multisource_column_metadata
FIELD: 'field',
MS_TABLE_FIELD: 'ms_table_field',
// Query constants
NSAMEAS: 'NSAMEAS',
LT_FIELD: 'LT_FIELD',
IN: 'IN',
initialize: function () {
this.classConfig = this._getMultisourceClassConfig();
this.columnMetadataMapping = this._populateColumnMapping();
this.MULTISOURCE_REPORT_MAX_LIMIT = gs.getProperty(this.MULTISOURCE_REPORT_MAX_LIMIT_PROPERTY, 100000);
this.DISCOVERY_SOURCE_NOT_REPORTING_MAX_DAYS = gs.getProperty(this.DISCOVERY_SOURCE_NOT_REPORTING_MAX_DAYS_PROPERTY, 7);
// Default value of max aggregates is from GlideAggregate.java
this.MAX_SUPPORTED_GLIDE_AGGREGATES = parseInt(gs.getProperty(this.GLIDE_AGGREGATES_MAX_LIMIT_PROPERTY, 100000));
},
populateCIsBySource: function() {
// Cleanup the existing aggregated entries
// If there any records in the aggregate table in "Draft" state, mark them as "Retired"
this._updateState(this.DRAFT, this.RETIRED, this.MULTISOURCE_DASHBOARD_DATA, this.SOURCE_OVERVIEW);
// Calculate the aggregate stats and populate in dashboard data with key as "Source_Overview"
var agg = new GlideAggregate(this.MULTISOURCE_DATA);
agg.addAggregate('COUNT', 'cmdb_ci');
agg.groupBy('discovery_source');
agg.setCategory(this.READ_REPLICA_CATEGORY);
agg.query();
while (agg.next()) {
var ciCount = agg.getAggregate('COUNT', 'cmdb_ci');
var discoverySource = agg.getValue('discovery_source');
// Insert a record in draft state
this._populateMultisourceDashboardData(this.SOURCE_OVERVIEW, discoverySource, ciCount, this.DRAFT);
}
// Update the existing records in "Ready" state to "Retired"
this._updateState(this.READY, this.RETIRED, this.MULTISOURCE_DASHBOARD_DATA, this.SOURCE_OVERVIEW);
// Update the existing records in "Draft" state to "Ready"
this._updateState(this.DRAFT, this.READY, this.MULTISOURCE_DASHBOARD_DATA, this.SOURCE_OVERVIEW);
},
populateCIsGroupedByDiscoverySource: function() {
// Max discovery source number counts i.e. 1, 2, 3, 4 and 5+
var maxSources = 5;
var totalCICount = 0;
var sourceNumberCounts = [];
// Cleanup the existing aggregated discovery source counts for CIs
// If there any records in the aggregate table in "Draft" state, mark them as "Retired"
this._updateState(this.DRAFT, this.RETIRED, this.MULTISOURCE_DASHBOARD_DATA, this.CHART_CIS_BY_NUMBER_OF_SOURCES);
// If there any records in the data drill down table in "Draft" state, mark them as "Retired"
// We need this because the new records we will create further will be in "Draft" state by default and we move them to "Ready" state.
this._updateState(this.DRAFT, this.RETIRED, this.CIS_BY_NUMBER_OF_SOURCES);
// Populate aggregate discovery source counts for CIs in 'sn_cmdb_ws_ms_ci_by_number_source'
// These counts are used to show the reports in CMDB Workspace
for (var i = 1; i <= maxSources; i++) {
var ciCount = 0;
var sourceCount = (i == maxSources) ? i + "+" : i + "";
var start = 0, end = this.MAX_SUPPORTED_GLIDE_AGGREGATES - 1;
// Platform has a limitation on number of aggregates returned using query, workaround is to call multiple times with varying offset
// If glide aggregate returns less data rows than asked (end -start), it indicates there is no more data to query
// If it returned exact rows as asked, we need to query again as there is potential of more data available
do {
var singleBatchRowCount = 0;
ga = this._getAggregateByDiscoverySource(i, maxSources, start, end);
while (ga.next()) {
singleBatchRowCount++;
ciCount++;
}
start = end + 1;
end = end + this.MAX_SUPPORTED_GLIDE_AGGREGATES;
} while (singleBatchRowCount == this.MAX_SUPPORTED_GLIDE_AGGREGATES);
// Insert a record in draft state
this._populateMultisourceDashboardData(this.CHART_CIS_BY_NUMBER_OF_SOURCES, sourceCount, ciCount, this.DRAFT);
// Increase the total CI count
totalCICount += ciCount;
// Use this to get the max limit for each source number count once aggregate data is populated
var sourceNumberCount = {
"source_count": sourceCount,
"ci_count": ciCount
};
sourceNumberCounts.push(sourceNumberCount);
}
// Populate the max limit for each source number count
this._updateSourceNumberCountMaxLimit(sourceNumberCounts, totalCICount);
// Populate CI by discovery source number data (ci, class and discovery source count)
// These are displayed when user clicks on one of the categories in the donut on the report
for (var j = 1; j <= maxSources; j++) {
var count = 0;
var ga;
var ciByNumberSourceDataGr = new GlideRecord(this.CIS_BY_NUMBER_OF_SOURCES);
/**
* Check the below conditions before inserting into 'sn_cmdb_ws_ms_ci_by_number_source_data' table
* 1. If total CI (for all discovery source number) count is less than max report limit, populate everyting.
* 2. If the class config is not defined, populate upto the max limit per discovery source number
* 3. Otherwise, iterate over the class config and follow the below steps:
* a. Calculate the max limit using the class config percent value
* b. Populate till max limit is reached and repeat the same for all classes in the config
*/
if (totalCICount < this.MULTISOURCE_REPORT_MAX_LIMIT) {
gs.debug("Total CI count: " + totalCICount + " is less than sn_cmdb_ws.ms.report_max_limit: " + this.MULTISOURCE_REPORT_MAX_LIMIT);
gs.debug("Getting all records from cmdb_multisource_data for CIs with " + j + " source(s).");
start = 0, end = this.MAX_SUPPORTED_GLIDE_AGGREGATES - 1;
// Read data in batches with varying offsets and exhaust all the data from database
do {
var singleBatchRowCount = 0;
ga = this._getAggregateByDiscoverySource(j, maxSources, start, end);
while (ga.next()) {
this._populateNumberSourceData(ciByNumberSourceDataGr, ga, j, maxSources);
singleBatchRowCount++;
}
start = end + 1;
end = end + this.MAX_SUPPORTED_GLIDE_AGGREGATES;
} while (singleBatchRowCount == this.MAX_SUPPORTED_GLIDE_AGGREGATES);
} else if (this.classConfig.length == 0) {
gs.debug("Total CI count: " + totalCICount + " is greater/equal than sn_cmdb_ws.ms.report_max_limit: " + this.MULTISOURCE_REPORT_MAX_LIMIT);
gs.debug(this.MULTISOURCE_CLASS_METADATA + " is empty. Getting max of " + sourceNumberCounts[j - 1].max_limit + " records for CIs with " + j + " source(s)");
count = 0;
start = 0, end = this.MAX_SUPPORTED_GLIDE_AGGREGATES - 1;
// Read data in batches with varying offsets and exhaust all the data from database
do {
var singleBatchRowCount = 0;
ga = this._getAggregateByDiscoverySource(j, maxSources, start, end);
while (ga.next() && count < sourceNumberCounts[j - 1].max_limit) {
this._populateNumberSourceData(ciByNumberSourceDataGr, ga, j, maxSources);
count++;
singleBatchRowCount++;
}
start = end + 1;
end = end + this.MAX_SUPPORTED_GLIDE_AGGREGATES;
} while (singleBatchRowCount == this.MAX_SUPPORTED_GLIDE_AGGREGATES);
gs.debug("Inserted " + count + " records for CIs with " + j + " source(s)");
} else {
gs.debug("Total CI count: " + totalCICount + " is greater/equal than sn_cmdb_ws.ms.report_max_limit: " + this.MULTISOURCE_REPORT_MAX_LIMIT);
gs.debug(this.MULTISOURCE_CLASS_METADATA + " is not empty");
for (k = 0; k < this.classConfig.length; k++) {
var classConfigMaxLimit = Math.round(sourceNumberCounts[j - 1].max_limit * (this.classConfig[k].ci_count_percent / 100));
count = 0;
start = 0, end = this.MAX_SUPPORTED_GLIDE_AGGREGATES - 1;
// Read data in batches with varying offsets and exhaust all the data from database
do {
var singleBatchRowCount = 0;
ga = this._getAggregateByDiscoverySource(j, maxSources, start, end, this.classConfig[k].table);
gs.debug("Getting max of " + classConfigMaxLimit + " records for " + this.classConfig[k].table + " for CIs with " + j + " source(s)");
while (ga.next() && count < classConfigMaxLimit) {
this._populateNumberSourceData(ciByNumberSourceDataGr, ga, j, maxSources);
count++;
singleBatchRowCount++;
}
start = end + 1;
end = end + this.MAX_SUPPORTED_GLIDE_AGGREGATES;
} while (singleBatchRowCount == this.MAX_SUPPORTED_GLIDE_AGGREGATES);
gs.debug("Inserted " + count + " records for " + this.classConfig[k].table + " for CIs with " + j + " source(s)");
}
}
}
// Update the existing records in aggregate dashboard table from "Ready" state to "Retired"
this._updateState(this.READY, this.RETIRED, this.MULTISOURCE_DASHBOARD_DATA, this.CHART_CIS_BY_NUMBER_OF_SOURCES);
// Mark the existing records in raw data table from "Ready" state to "Retired"
this._updateState(this.READY, this.RETIRED, this.CIS_BY_NUMBER_OF_SOURCES);
// Update the newly created aggregate records in "Draft" state to "Ready"
this._updateState(this.DRAFT, this.READY, this.MULTISOURCE_DASHBOARD_DATA, this.CHART_CIS_BY_NUMBER_OF_SOURCES);
// Mark the existing records in raw data table from "Draft" state to "Ready"
this._updateState(this.DRAFT, this.READY, this.CIS_BY_NUMBER_OF_SOURCES);
},
populateCIsCoverageForSingleSource: function () {
// Cleanup the existing aggregated entries
// If there any records in the aggregate table in "Draft" state, mark them as "Retired"
this._updateState(this.DRAFT, this.RETIRED, this.MULTISOURCE_DASHBOARD_DATA, this.CHART_CIS_WITH_SINGLE_SOURCE);
// If there any records in the data drill down table in "Draft" state, mark them as "Retired"
// We need this because the new records we will create further will be in "Draft" state by default and we move them to "Ready" state.
this._updateState(this.DRAFT, this.RETIRED, this.CIS_WITH_SINGLE_SOURCE);
// Populate number of CIs per source
var sources = {};
var totalCICount = 0;
// Read data in batches with varying offsets and exhaust all the data from database
var start = 0, end = this.MAX_SUPPORTED_GLIDE_AGGREGATES - 1;
do {
var singleBatchRowCount = 0;
var agg = this._getAggregateSingleSource(null, start, end);
while (agg.next()) {
var source = agg.getAggregate('MAX', this.DISCOVERY_SOURCE);
if (typeof sources[source] === 'undefined') {
sources[source] = 1;
} else {
sources[source] = sources[source] + 1;
}
totalCICount++;
singleBatchRowCount++;
}
start = end + 1;
end = end + this.MAX_SUPPORTED_GLIDE_AGGREGATES;
} while (singleBatchRowCount == this.MAX_SUPPORTED_GLIDE_AGGREGATES);
// Populate aggregate data by source and raw data only if there is atleast one CI with a single discovery source
if (Object.keys(sources).length > 0) {
// Insert into intermediate aggregate table
for (source in sources) {
gs.debug(source + "= " + sources[source]);
// Insert a record in draft state
this._populateMultisourceDashboardData(this.CHART_CIS_WITH_SINGLE_SOURCE, source, sources[source], this.DRAFT);
}
// Call populating raw data only if there is atleast one CI with a single discovery source
this._populateRawDataCoverageForSingleSource(totalCICount, sources);
} else {
// Found 0 CIs with single discovery source, populating an empty record for UI
gs.info("Found 0 CIs with single discovery source from multisource data, not populating raw data");
this._populateMultisourceDashboardData(this.CHART_CIS_WITH_SINGLE_SOURCE, this.ZERO_CIS_WITH_A_SINGLE_SOURCE_KEY, this.ZERO_CIS_WITH_A_SINGLE_SOURCE_VALUE, this.DRAFT);
}
// Update the existing aggregate records in dashboard data table from "Ready" state to "Retired"
this._updateState(this.READY, this.RETIRED, this.MULTISOURCE_DASHBOARD_DATA, this.CHART_CIS_WITH_SINGLE_SOURCE);
// Mark the existing raw data records in data drill down table from "Ready" state to "Retired"
this._updateState(this.READY, this.RETIRED, this.CIS_WITH_SINGLE_SOURCE);
// Update the existing aggregate records in dashboard data table from "Draft" state to "Ready"
this._updateState(this.DRAFT, this.READY, this.MULTISOURCE_DASHBOARD_DATA, this.CHART_CIS_WITH_SINGLE_SOURCE);
// Mark the existing raw data records in data drill down table from "Draft" state to "Ready"
this._updateState(this.DRAFT, this.READY, this.CIS_WITH_SINGLE_SOURCE);
},
//Populates the Number of discovery sources in the sn_cmdb_ws_ms_ci_dashboard_data table
populateNumberOfDiscoverySources: function() {
// Update the existing records in "Draft" state to "Retired"
this._updateState(this.DRAFT, this.RETIRED, this.MULTISOURCE_DASHBOARD_DATA, this.NUMBER_OF_DISCOVERY_SOURCES);
// Insert a record in draft state
var count = this._getNumberOfDiscoverySources();
this._populateMultisourceDashboardData(this.NUMBER_OF_DISCOVERY_SOURCES, this.NUMBER_OF_DISCOVERY_SOURCES, count, this.DRAFT);
// Update the existing records in "Ready" state to "Retired"
this._updateState(this.READY, this.RETIRED, this.MULTISOURCE_DASHBOARD_DATA, this.NUMBER_OF_DISCOVERY_SOURCES);
// Update the existing records in "Draft" state to "Ready"
this._updateState(this.DRAFT, this.READY, this.MULTISOURCE_DASHBOARD_DATA, this.NUMBER_OF_DISCOVERY_SOURCES);
},
//Populates the Raw records in the sn_cmdb_ws_ms_ci_dashboard_data table
populateRawRecords: function() {
// Update the existing records in "Draft" state to "Retired"
this._updateState(this.DRAFT, this.RETIRED, this.MULTISOURCE_DASHBOARD_DATA, this.RAW_RECORDS);
// Insert a record in draft state
var count = this._getRawRecords();
this._populateMultisourceDashboardData(this.RAW_RECORDS, this.RAW_RECORDS, count, this.DRAFT);
// Update the existing records in "Ready" state to "Retired"
this._updateState(this.READY, this.RETIRED, this.MULTISOURCE_DASHBOARD_DATA, this.RAW_RECORDS);
// Update the existing records in "Draft" state to "Ready"
this._updateState(this.DRAFT, this.READY, this.MULTISOURCE_DASHBOARD_DATA, this.RAW_RECORDS);
},
//Populates the Reconciled CIs in the sn_cmdb_ws_ms_ci_dashboard_data table
populateReconciledCIs: function() {
// Update the existing records in "Draft" state to "Retired"
this._updateState(this.DRAFT, this.RETIRED, this.MULTISOURCE_DASHBOARD_DATA, this.RECONCILED_CIS);
// Insert a record in draft state
var count = this._getReconciledCIs();
this._populateMultisourceDashboardData(this.RECONCILED_CIS, this.RECONCILED_CIS, count, this.DRAFT);
// Update the existing records in "Ready" state to "Retired"
this._updateState(this.READY, this.RETIRED, this.MULTISOURCE_DASHBOARD_DATA, this.RECONCILED_CIS);
// Update the existing records in "Draft" state to "Ready"
this._updateState(this.DRAFT, this.READY, this.MULTISOURCE_DASHBOARD_DATA, this.RECONCILED_CIS);
},
populateDiscoverySourcesNotReportingCIs: function() {
// If there any records in the data drill down and aggregate tables in "Draft" state, mark them as "Retired"
// We need this because the new records we will create further will be in "Draft" state by default and we move them to "Ready" state.
this._updateState(this.DRAFT, this.RETIRED, this.DISCOVERY_SOURCES_NOT_REPORTING);
this._updateState(this.DRAFT, this.RETIRED, this.MULTISOURCE_DASHBOARD_DATA, this.CHART_DISCOVERY_SOURCE_NOT_REPORTING);
// Populate the aggregate count for discovery sources not reporting
var aggCount = this._evaluateCIsNotReportedByDiscoverySources(0, true);
this._populateMultisourceDashboardData(this.CHART_DISCOVERY_SOURCE_NOT_REPORTING, this.CHART_DISCOVERY_SOURCE_NOT_REPORTING, aggCount, this.DRAFT);
/**
* Check the below conditions before inserting into 'sn_cmdb_ws_ms_discovery_sources_not_reporting' table
* 1. If the class config is not defined or aggregate count is less than max limit, populate upto the max limit defined in 'sn_cmdb_ws.ms.report_max_limit'
* 2. Otherwise, iterate over the class config and follow the below steps:
* a. Calculate the max limit using the class config percent value
* b. Populate till max limit is reached and repeat the same for all classes in the config
*/
if (aggCount < this.MULTISOURCE_REPORT_MAX_LIMIT || this.classConfig.length == 0) {
gs.debug(this.MULTISOURCE_CLASS_METADATA + " is empty or total records is less than max limit. Getting max of " + this.MULTISOURCE_REPORT_MAX_LIMIT
+ " records for CIs not reported by dicovery source in the past " + this.DISCOVERY_SOURCE_NOT_REPORTING_MAX_DAYS + " days.");
this._evaluateCIsNotReportedByDiscoverySources(this.MULTISOURCE_REPORT_MAX_LIMIT, false);
} else {
gs.debug(this.MULTISOURCE_CLASS_METADATA + " is empty. Getting max of " + this.MULTISOURCE_REPORT_MAX_LIMIT
+ " records for CIs not reported by dicovery source in the past " + this.DISCOVERY_SOURCE_NOT_REPORTING_MAX_DAYS + " days.");
for (var i = 0; i < this.classConfig.length; i++) {
var classConfigMaxLimit = Math.round(this.MULTISOURCE_REPORT_MAX_LIMIT * (this.classConfig[i].ci_count_percent / 100));
this._evaluateCIsNotReportedByDiscoverySources(classConfigMaxLimit, false, this.classConfig[i].table);
}
}
// Mark the existing raw data in "Ready" state stored for data drill down and aggregate tables as "Retired"
this._updateState(this.READY, this.RETIRED, this.DISCOVERY_SOURCES_NOT_REPORTING);
this._updateState(this.READY, this.RETIRED, this.MULTISOURCE_DASHBOARD_DATA, this.CHART_DISCOVERY_SOURCE_NOT_REPORTING);
// Mark the current raw data in "Draft" state stored for data drill down and agregate tables as "Ready"
this._updateState(this.DRAFT, this.READY, this.DISCOVERY_SOURCES_NOT_REPORTING);
this._updateState(this.DRAFT, this.READY, this.MULTISOURCE_DASHBOARD_DATA, this.CHART_DISCOVERY_SOURCE_NOT_REPORTING);
},
/**
* Populates the aggregate count of data mismatch records in sn_cmdb_ws_ms_ci_dashboard_data table with key as data_mismatch
* Populates the raw data mismatch records based on data mismatch settings in sn_cmdb_ws_ms_data_mismatch
*/
populateDataMismatch: function () {
// If there any records in the data drill down table in "Draft" state, mark them as "Retired"
// We need this because the new records we will create further will be in "Draft" state by default and we move them to "Ready" state.
this._updateState(this.DRAFT, this.RETIRED, this.MULTISOURCE_DASHBOARD_DATA, this.CHART_DATA_MISMATCH);
this._updateState(this.DRAFT, this.RETIRED, this.DATA_MISMATCH);
// Calculate aggregate data mismatch count and insert with draft state
var count = this._calculateDataMismatchAggregateCount();
this._populateMultisourceDashboardData(this.CHART_DATA_MISMATCH, this.CHART_DATA_MISMATCH, count, this.DRAFT);
// Populates raw data mismatch records in sn_cmdb_ws_ms_data_mismatch table by leveraging sn_cmdb_ws_ms_data_mismatch_config
this._populateDataMismatchRawData(count);
// Update the existing records in "Ready" state to "Retired" for both aggregated and raw data tables
this._updateState(this.READY, this.RETIRED, this.MULTISOURCE_DASHBOARD_DATA, this.CHART_DATA_MISMATCH);
this._updateState(this.READY, this.RETIRED, this.DATA_MISMATCH);
// Update the existing records in "Draft" state to "Ready" for both aggregated and raw data tables
this._updateState(this.DRAFT, this.READY, this.MULTISOURCE_DASHBOARD_DATA, this.CHART_DATA_MISMATCH);
this._updateState(this.DRAFT, this.READY, this.DATA_MISMATCH);
},
/**
* Populate raw data for CIs with a single discovery source
* @param {*} totalCICount total number of CIs which are discovered by single discovery source
* @param {*} sources map of source to number of CIs discovered by single discovery source
* Example of sources= {'ServiceNow'-> 10, 'ServiceWatch' -> 20}
*/
_populateRawDataCoverageForSingleSource: function (totalCICount, sources) {
// Logic for populating raw data starts here
var rawGr = new GlideRecord(this.CIS_WITH_SINGLE_SOURCE);
/**
* Check the below conditions before inserting into 'sn_cmdb_ws_ms_ci_coverage_data' table
* 1. If total CI count is less than max report limit, populate everyting.
* 2. If the class config is not defined, populate upto the max limit per discovery source number
* 3. Otherwise, iterate over the class config and follow the below steps:
* a. Calculate the max limit using the class config percent value
* b. Populate till max limit is reached and repeat the same for all classes in the config
*/
if (totalCICount < this.MULTISOURCE_REPORT_MAX_LIMIT) {
gs.debug("Total CI count: " + totalCICount + " is less than sn_cmdb_ws.ms.report_max_limit: " + this.MULTISOURCE_REPORT_MAX_LIMIT);
// Read data in batches with varying offsets and exhaust all the data from database
var start =0, end = this.MAX_SUPPORTED_GLIDE_AGGREGATES -1 ;
do {
var singleBatchRowCount = 0;
var agg = this._getAggregateSingleSource(null, start, end);
while (agg.next()) {
this._populateRawCIsCoverageBySingleSource(rawGr, agg);
singleBatchRowCount++;
}
start = end + 1;
end = end + this.MAX_SUPPORTED_GLIDE_AGGREGATES;
} while (singleBatchRowCount == this.MAX_SUPPORTED_GLIDE_AGGREGATES);
}
else if (this.classConfig.length == 0) {
gs.debug("Total CI count: " + totalCICount + " is greater than sn_cmdb_ws.ms.report_max_limit: " + this.MULTISOURCE_REPORT_MAX_LIMIT);
gs.debug(this.MULTISOURCE_CLASS_METADATA + " is empty.");
// Max CIs to be inserted per source
var sourceMaxCounts = {};
this._updateSourceMaxCountsSingleSource(totalCICount, this.MULTISOURCE_REPORT_MAX_LIMIT, sources, sourceMaxCounts);
// Read data in batches with varying offsets and exhaust all the data from database
var readWriteCounts = {};
start = 0, end = this.MAX_SUPPORTED_GLIDE_AGGREGATES -1;
do {
var agg = this._getAggregateSingleSource(null, start, end);
readWriteCounts = this._populateRawDataSingleSource(agg, rawGr, sourceMaxCounts, this.MULTISOURCE_REPORT_MAX_LIMIT, readWriteCounts);
start = end + 1;
end = end + this.MAX_SUPPORTED_GLIDE_AGGREGATES;
} while (readWriteCounts[this.READ_COUNT] == this.MAX_SUPPORTED_GLIDE_AGGREGATES);
}
else {
gs.debug("Total CI count: " + totalCICount + " is greater than sn_cmdb_ws.ms.report_max_limit: " + this.MULTISOURCE_REPORT_MAX_LIMIT);
gs.debug(this.MULTISOURCE_CLASS_METADATA + " is not empty.");
for (i = 0; i < this.classConfig.length; i++) {
// Max CIs to be inserted per source
var sourceMaxCounts = {};
// Calculate total CIs to be inserted in raw data table for a class using the percent field defined
var classMaxLimit = this.MULTISOURCE_REPORT_MAX_LIMIT * this.classConfig[i].ci_count_percent * 0.01;
this._updateSourceMaxCountsSingleSource(totalCICount, classMaxLimit, sources, sourceMaxCounts);
var maxRecordsToInsert = this._getMaxRecordsToInsertSingleSource(sourceMaxCounts);
// Read data in batches with varying offsets and exhaust all the data from database
start = 0, end = this.MAX_SUPPORTED_GLIDE_AGGREGATES - 1;
var readWriteCounts = {};
do {
var agg = this._getAggregateSingleSource(this.classConfig[i].table, start, end);
readWriteCounts = this._populateRawDataSingleSource(agg, rawGr, sourceMaxCounts, maxRecordsToInsert, readWriteCounts);
start = end + 1;
end = end + this.MAX_SUPPORTED_GLIDE_AGGREGATES;
} while (readWriteCounts[this.READ_COUNT] == this.MAX_SUPPORTED_GLIDE_AGGREGATES);
}
}
},
/**
* Populate the sn_cmdb_ws_ms_ci_coverage_data table
* sourceMaxCounts: Max CI count for each source
* maxRecordsToInsert: max records to be inserted in the sn_cmdb_ws_ms_ci_coverage_data table
* If number of records reaches to the max records to be inserted, exit the while loop
* When aggreagte query returns 1000 records
* And maxRecordsToInsert is 100
* Then exit while loop when currCount reaches 100
* Populate number of records, read using this glide aggregate query, records written to raw table in readWriteCounts
*/
_populateRawDataSingleSource: function(agg, rawGr, sourceMaxCounts, maxRecordsToInsert, readWriteCounts) {
var batchRowCount = 0;
var totalWriteCount = 0;
if (readWriteCounts && readWriteCounts[this.WRITE_COUNT]) {
totalWriteCount = readWriteCounts[this.WRITE_COUNT];
} else {
readWriteCounts = {};
}
while (agg.next()) {
var source = agg.getAggregate('MAX', this.DISCOVERY_SOURCE);
batchRowCount++;
if (sourceMaxCounts[source].current_count < sourceMaxCounts[source].max_count) {
this._populateRawCIsCoverageBySingleSource(rawGr, agg);
totalWriteCount++;
sourceMaxCounts[source].current_count++;
}
if (totalWriteCount === maxRecordsToInsert)
break;
}
readWriteCounts[this.READ_COUNT] = batchRowCount;
readWriteCounts[this.WRITE_COUNT] = totalWriteCount;
return readWriteCounts;
},
/**
* Returns max records to be inserted in sn_cmdb_ws_ms_ci_coverage_data table
*/
_getMaxRecordsToInsertSingleSource: function(sourceCis) {
var totalRawCount = 0;
for (var source in sourceCis) {
totalRawCount += sourceCis[source].max_count;
}
return totalRawCount;
},
/**
* Updates the max CI count per source and initialize the current count
* If there are 200 total CIs in which 150 are from ServiceNow and 50 are from ServiceWatch
* If the MULTISOURCE_REPORT_MAX_LIMIT is set to 100, this function will return the below object:
* {
* "ServiceNow": { "max_count": "75", "current_count": "0" }",
* "ServiceWatch": { "max_count": "25", "current_count": "0" }"
* }
*/
_updateSourceMaxCountsSingleSource: function(totalCount, totalCis, sources, sourceMaxCounts) {
for (var source in sources) {
var ciCount = {};
var sourcePercent = (sources[source]/totalCount) * 100;
ciCount[this.MAX_COUNT] = Math.round(totalCis * sourcePercent * 0.01);
ciCount[this.CURRENT_COUNT] = 0;
sourceMaxCounts[source] = ciCount;
}
},
/**
* Returns the aggregate query result
*/
_getAggregateSingleSource: function(className, start, end) {
var agg = new GlideAggregate(this.MULTISOURCE_DATA);
if (className) {
agg.addQuery(this.CLASS_NAME, 'INSTANCEOF', className);
}
agg.addAggregate('MAX', this.DISCOVERY_SOURCE);
agg.groupBy(this.CI);
agg.addHaving('COUNT', this.DISCOVERY_SOURCE, '=', '1');
agg.chooseWindow(start, end);
agg.setCategory(this.READ_REPLICA_CATEGORY);
agg.query();
return agg;
},
/**
* Insert a record into the sn_cmdb_ws_ms_ci_coverage_data table
*/
_populateRawCIsCoverageBySingleSource: function(rawGr, agg) {
rawGr.initialize();
rawGr.setValue(this.SOURCE, agg.getAggregate('MAX', this.DISCOVERY_SOURCE));
rawGr.setValue(this.CLASS_NAME, agg.getValue(this.CLASS_NAME));
rawGr.setValue(this.CI, agg.getValue(this.CI));
/**
* Group by CI (Document Id) is currently not working in Multisource dashboard,
* we need to remove this once that defect is fixed
* Populating non-cmdb Sys Id into the cmdb_reference field for grouping purpose
* And also to optimize the check for hierarchy at every row
* UI will have cmdb_reference column instead of Document Id until the defect is fixed
*/
rawGr.setValue(this.CMDB_REFERENCE, agg.getValue(this.CI));
rawGr.insert();
},
/**
* Gets the aggregate object for multisource records grouped by CI for each discovery source count
* Adds a query for class name if passed as a parameter to the function
* Add window for the query results (start, end), it will return a maximum rows of (end -start)
* If glide aggregate returns rows equal to the size(end -start), it indicates more potential data
*/
_getAggregateByDiscoverySource: function(sourceCount, maxSourceCount, start, end, className) {
var agg = new GlideAggregate(this.MULTISOURCE_DATA);
if (className)
agg.addQuery(this.CLASS_NAME, 'INSTANCEOF', className);
agg.groupBy(this.CI);
/**
* maxSourceCount is the maximum number of discovery sources we have for this report i.e. 5
* sourceCount is the current number of discovery sources we want to query i.e. one of 1, 2, 3, 4 or 5+
*
* For CIs with sourceCount 1, 2, 3 and 4, check for exact match e.g. HAVING COUNT(discovery_source) = 2
* For CIs with sourceCount 5+, check for count greater than 4 e.g. HAVING COUNT(discovery_source) > 4
*/
agg.addHaving('COUNT', this.DISCOVERY_SOURCE, sourceCount == maxSourceCount ? '>' : '=', sourceCount == maxSourceCount ? (maxSourceCount - 1) : sourceCount);
agg.chooseWindow(start, end);
agg.setCategory(this.READ_REPLICA_CATEGORY);
agg.query();
return agg;
},
/**
* Populate the sn_cmdb_ws_ms_ci_by_number_source_data table for passed source count i.e. 1, 2, 3, 4, and 5+
*/
_populateNumberSourceData: function(ciByNumberSourceDataGr, ga, sourceCount, maxSources) {
ciByNumberSourceDataGr.initialize();
ciByNumberSourceDataGr.setValue(this.SOURCE_COUNT, sourceCount == maxSources ? sourceCount + "+" : sourceCount + "");
ciByNumberSourceDataGr.setValue(this.CLASS_NAME, ga.getValue(this.CLASS_NAME));
ciByNumberSourceDataGr.setValue(this.CI, ga.getValue(this.CI));
/**
* Group by CI (Document Id) is currently not working in Multisource dashboard,
* we need to remove this once that defect is fixed
* Populating non-cmdb Sys Id into the cmdb_reference field for grouping purpose
* And also to optimize the check for hierarchy at every row
* UI will have cmdb_reference column instead of Document Id until the defect is fixed
*/
ciByNumberSourceDataGr.setValue(this.CMDB_REFERENCE, ga.getValue(this.CI));
ciByNumberSourceDataGr.insert();
},
/**
* Update the max counts for each source number count using the MULTISOURCE_REPORT_MAX_LIMIT property
* Updates an array of objects where each object contains a source_count, ci_count and max_limit
*
* If there are 100 total CIs in which 70 are from 3 discovery sources and 30 are from 5+ discovery sources
* If the MULTISOURCE_REPORT_MAX_LIMIT is set to 50, this function will return the below array:
* [
* {
* "source_count": "3",
* "ci_count": "70",
* "max_limit": "35",
* },
* {
* "source_count": "5+",
* "ci_count": "30",
* "max_limit": "15",
* }
* ]
*/
_updateSourceNumberCountMaxLimit: function(sourceNumberCounts, totalCICount) {
for (var i = 0; i < sourceNumberCounts.length; i++) {
var percent = Math.round((sourceNumberCounts[i].ci_count * 100) / totalCICount);
sourceNumberCounts[i].max_limit = Math.round(this.MULTISOURCE_REPORT_MAX_LIMIT * (percent / 100));
}
},
/**
* Creates an array of objects to store the class metadata for multisource reports
* Each object contains the table name and its ci_count_percent (percentage of CIs to be included)
*/
_getMultisourceClassConfig: function() {
var configs = [];
var gr = new GlideRecord(this.MULTISOURCE_CLASS_METADATA);
gr.query();
while (gr.next()) {
var config = {
"table": gr.getValue(this.TABLE),
"ci_count_percent": gr.getValue(this.CI_COUNT_PERCENT)
};
configs.push(config);
}
return configs;
},
_updateState: function(sourceState, targetState, table, chart) {
var gr = new GlideRecord(table);
if(chart)
gr.addQuery(this.CHART, chart);
gr.addQuery(this.STATE, sourceState);
gr.setValue(this.STATE, targetState);
gr.updateMultiple();
},
//Returns the number of Raw records
_getRawRecords: function() {
var agg = new GlideAggregate(this.MULTISOURCE_DATA);
agg.addAggregate('COUNT');
agg.setCategory(this.READ_REPLICA_CATEGORY);
agg.query();
var records = 0;
if (agg.next())
records = agg.getAggregate('COUNT');
return records;
},
//Returns the number of Reconciled CIs
_getReconciledCIs: function() {
var agg = new GlideAggregate(this.MULTISOURCE_DATA);
agg.addAggregate('COUNT(DISTINCT', this.CI);
agg.setGroup(false);
agg.setCategory(this.READ_REPLICA_CATEGORY);
agg.query();
var records = 0;
if (agg.next())
records = agg.getAggregate('COUNT(DISTINCT', this.CI);
return records;
},
//Returns the number of Discovery sources
_getNumberOfDiscoverySources: function() {
var agg = new GlideAggregate(this.MULTISOURCE_DATA);
agg.addAggregate('COUNT(DISTINCT', this.DISCOVERY_SOURCE);
agg.setGroup(false);
agg.setCategory(this.READ_REPLICA_CATEGORY);
agg.query();
var records = 0;
if (agg.next())
records = agg.getAggregate('COUNT(DISTINCT', this.DISCOVERY_SOURCE);
return records;
},
/**
* Populate the sn_cmdb_ws_ms_ci_dashboard_data table
* chart: Chart on the Multisource Dashboard
* key: Key in the chart
* value: Value in the chart
* state: State in the record i.e. Draft, Ready, Retired
* Example
* chart: Number of discovery sources
* key: number_of_discovery_sources
* value: 15
* state: Draft
*/
_populateMultisourceDashboardData: function(chart, key, value, state) {
var gr = new GlideRecord(this.MULTISOURCE_DASHBOARD_DATA);
gr.initialize();
gr.setValue(this.CHART, chart);
gr.setValue(this.KEY, key);
gr.setValue(this.VALUE, value);
gr.setValue(this.STATE, state);
gr.insert();
},
/**
* Evaluate if a discovery source is not reporting a CI for 'n' number of days
*
* @param limit The total number of records to compute the discovery sources not reporting (0 for aggreagte count)
* @param isCountOnly Flag to indicate this method is called for data count or populating actual data
* @param className Class name to compute the discovery sources not reporting
*/
_evaluateCIsNotReportedByDiscoverySources: function(limit, isCountOnly, className) {
var count = 0;
var ci, lastDiscovered, maxLastDiscovered;
// Get the multisource column mapping for 'last_discovered' in 'cmdb_multisource_column_metadata'
var lastDiscoveredColumnMapping = this._getIndexedColumn(this.CMDB_CI, this.LAST_DISCOVERED);
/**
* Query the 'cmdb_multisource_data' and sort by ci and last_discovered (in descending order).
* Compare last_discovered for all other rows for of that CI with the max last_discovered.
* Populate the 'sn_cmdb_ws_ms_discovery_sources_not_reporting' table where the source is not reporting.
*/
var gr = this._getCIsNotReportedByDiscoverySources(className != null ? className : this.CMDB_CI, lastDiscoveredColumnMapping);
while (gr.next()) {
// Break if we do not want the count of all records and have reached the limit
if (count >= limit && !isCountOnly)
break;
// Check if the current CI is same as the previos CI.
// If not, it means we have to calculate for a different CI. Reset the ci and maxLastDiscovered values
if (ci != gr.getValue(this.CI)) {
ci = gr.getValue(this.CI);
maxLastDiscovered = gr.getValue(lastDiscoveredColumnMapping);
continue;
}
// If the lastDiscovered is older than maxLastDiscovered, populate the 'sn_cmdb_ws_ms_discovery_sources_not_reporting' table
lastDiscovered = gr.getValue(lastDiscoveredColumnMapping);
if (this._isLastDiscoveredOlderThanMaxDays(lastDiscovered, maxLastDiscovered)) {
// Populate the 'sn_cmdb_ws_ms_discovery_sources_not_reporting' only when isCountOnly flag is false
if (count < limit && !isCountOnly)
this._populateDiscoverySourceNotReportingData(ci, gr.getValue(this.CLASS_NAME), gr.getValue(this.DISCOVERY_SOURCE), lastDiscovered, className);
count++;
}
}
return count;
},
/**
* Get the potential list of CIs not reported by discovery sources
* Sort by the last_discovered in descending order so that max value is displayed first
*/
_getCIsNotReportedByDiscoverySources: function(className, lastDiscoveredColumnMapping) {
var gr = new GlideRecord(this.MULTISOURCE_DATA);
gr.addQuery(this.CLASS_NAME, 'INSTANCEOF', className);
gr.addNotNullQuery(lastDiscoveredColumnMapping);
gr.orderBy(this.CI);
gr.orderByDesc(lastDiscoveredColumnMapping);
gr.setCategory(this.READ_REPLICA_CATEGORY);
gr.query();
return gr;
},
/**
* Compare if the last_discovered is older than the max last_discovered by 'n' number of days
*/
_isLastDiscoveredOlderThanMaxDays: function(lastDiscovered, maxLastDiscovered) {
var gd1 = new GlideDateTime(lastDiscovered);
var gd2 = new GlideDateTime(maxLastDiscovered);
var dur = GlideDateTime.subtract(gd1, gd2);
return dur.getRoundedDayPart() > this.DISCOVERY_SOURCE_NOT_REPORTING_MAX_DAYS;
},
/**
* Insert a record into the 'sn_cmdb_ws_ms_discovery_sources_not_reporting' table
*/
_populateDiscoverySourceNotReportingData: function(ci, classname, discoverySource, lastDiscovered) {
var gr = new GlideRecord(this.DISCOVERY_SOURCES_NOT_REPORTING);
gr.initialize();
gr.setValue(this.CI, ci);
/**
* Group by CI (Document Id) is currently not working in Multisource dashboard,
* we need to remove this once that defect is fixed
* Populating non-cmdb Sys Id into the cmdb_reference field for grouping purpose
* And also to optimize the check for hierarchy at every row
* UI will have cmdb_reference column instead of Document Id until the defect is fixed
*/
gr.setValue(this.CMDB_REFERENCE, ci);
gr.setValue(this.CLASS_NAME, classname);
gr.setValue(this.DISCOVERY_SOURCE, discoverySource);
gr.setValue(this.LAST_DISCOVERED, lastDiscovered);
gr.setValue(this.STATE, this.DRAFT);
gr.insert();
},
/**
* Reads data mismatch config table, if no configurations has been defined returns immediately( nothing to process)
* For every defined config, reads the attributes, condition_type and prepares an 'encoded query' to query multisource database view,
* adds additional query conditions to filter data and makes a glideaggregate call.
* Calculates the aggregate count for one config, store it and repeats the process for other configs.
* Sum of all aggregate counts by config is returned to the caller.
* @returns count of aggregate data mismatches
*/
_calculateDataMismatchAggregateCount: function() {
// read the data_mismatch_config table
var configs = this._getDataMismatchConfig();
if (configs.length == 0) {
gs.error("No configurations defined for data mismatch calculation, cannot calculate data mismatch counts");
return -1;
}
var totalRecords = 0;
// iterate over the config
for (var index = 0; index < configs.length; index++) {
var config = configs[index];
// prepare the encoded query with AND/OR condition
var encodedQuery = this._getEncodedDataMismatchQuery(config);
if (!encodedQuery || encodedQuery === '') {
gs.error("Error in building encoded query for config with " + global.JSON.stringify(config));
continue;
}
var agg = new GlideAggregate(this.CMDB_MULTISOURCE_DIFF_QUERY_VIEW);
agg.addQuery(this.CMDB_MULTISOURCE_DIFF_QUERY_VIEW_PREFIX_1 + this.CLASS_NAME, 'INSTANCEOF', config.table);
if (config.discovery_sources && config.discovery_sources !== '') {
agg.addQuery(this.CMDB_MULTISOURCE_DIFF_QUERY_VIEW_PREFIX_1 + this.DISCOVERY_SOURCE, this.IN, config.discovery_sources);
agg.addQuery(this.CMDB_MULTISOURCE_DIFF_QUERY_VIEW_PREFIX_2 + this.DISCOVERY_SOURCE, this.IN, config.discovery_sources);
}
// Add query to remove duplicate rows between discovery sources, follow only one order betwen sources (A < B)
agg.addQuery(this.CMDB_MULTISOURCE_DIFF_QUERY_VIEW_PREFIX_1 + this.DISCOVERY_SOURCE, this.LT_FIELD, this.CMDB_MULTISOURCE_DIFF_QUERY_VIEW_PREFIX_2 + this.DISCOVERY_SOURCE);
agg.addEncodedQuery(encodedQuery);
agg.addAggregate('COUNT');
agg.setCategory(this.READ_REPLICA_CATEGORY);
agg.query();
var records = 0;
// We issue one aggregate query by one data mismatch config
if (agg.next()) {
records = agg.getAggregate('COUNT');
gs.debug("Data mismatch records for class '" + config.table + "' with encodedQuery '" + encodedQuery + "' is " + records);
totalRecords += parseInt(records);
}
}
gs.debug("Total records for data mismatch: " + totalRecords);
return totalRecords;
},
/**
* Populates raw data mismatch records using data mismatch config
* Iterates over the data mismatch config and populate data mismatch records, if aggregate data mismatch count is greater than max rows to write,
* calculates the percentage of raw data mismatch records for each class and limits results to that limit
* @param {*} aggDataMismatchCount aggregate count of data mismatch records
* @returns
*/
_populateDataMismatchRawData: function(aggDataMismatchCount) {
// read the data_mismatch_config table
var configs = this._getDataMismatchConfig();
if (configs.length == 0) {
gs.error("No configurations defined for data mismatch calculation, cannot populate data mismatch records");
return;
}
// iterate over the config
for (var index = 0; index < configs.length; index++) {
var config = configs[index];
// prepare the encoded query with AND/OR condition
var encodedQuery = this._getEncodedDataMismatchQuery(config);
if (!encodedQuery || encodedQuery === '') {
gs.error("Error in building encoded query for config with " + global.JSON.stringify(config));
continue;
}
var maxRowsToWrite = this.MULTISOURCE_REPORT_MAX_LIMIT;
// When total aggregate data mismatch count is greater than max limit, we store partial data in raw mismatch table
if (aggDataMismatchCount > this.MULTISOURCE_REPORT_MAX_LIMIT) {
maxRowsToWrite = this.MULTISOURCE_REPORT_MAX_LIMIT * config.ci_count_percent / 100;
}
this._executeAndPopulateRawDataMismatch(encodedQuery, config, maxRowsToWrite);
}
},
/**
* Prepares the glide record with diff query view, adds encoded query based on input config, executes the query and store results
* If input config has available discovery sources, diff will be applied with only those discovery sources.
* If input config has no available discovery sources, diff will be applied for all discovery sources in the system
* @param {*} encodedQuery evaluated query condition using diff fields from config
* @param {*} config data mismatch config defined for one class ( diff fields, condition_type, percentage)
* @param {*} maxRowsToWrite maximum number of rows to write for this config
* @returns
*/
_executeAndPopulateRawDataMismatch: function(encodedQuery, config, maxRowsToWrite) {
var msGr = new GlideRecord(this.CMDB_MULTISOURCE_DIFF_QUERY_VIEW);
msGr.addQuery(this.CMDB_MULTISOURCE_DIFF_QUERY_VIEW_PREFIX_1 + this.CLASS_NAME, 'INSTANCEOF', config.table);
if (config.discovery_sources && config.discovery_sources !== '') {
msGr.addQuery(this.CMDB_MULTISOURCE_DIFF_QUERY_VIEW_PREFIX_1 + this.DISCOVERY_SOURCE, this.IN, config.discovery_sources);
msGr.addQuery(this.CMDB_MULTISOURCE_DIFF_QUERY_VIEW_PREFIX_2 + this.DISCOVERY_SOURCE, this.IN, config.discovery_sources);
}
// Add query to remove duplicate rows between discovery sources, follow only one order betwen sources (A < B)
msGr.addQuery(this.CMDB_MULTISOURCE_DIFF_QUERY_VIEW_PREFIX_1 + this.DISCOVERY_SOURCE, this.LT_FIELD, this.CMDB_MULTISOURCE_DIFF_QUERY_VIEW_PREFIX_2 + this.DISCOVERY_SOURCE);
msGr.addEncodedQuery(encodedQuery);
msGr.chooseWindow(0, maxRowsToWrite);
msGr.setCategory(this.READ_REPLICA_CATEGORY);
msGr.query();
var mappedColumnsArr = this._getMappedColumnsDataMismatch(config);
this._writeRawDataMismatchRecords(msGr, mappedColumnsArr, maxRowsToWrite);
},
/**
* Reads data from multisource diff query view glide record, populate data mismatch records from multisource glide record,
* Also populate the diff fields( use mapped columns to get the display name) and discovery sources
* @param {*} msGr GlideRecord of diff query view with all encoded conditions
* @param {*} mappedColumns Map of column -> { display name, column name}
* Ex: col51 -> fqdn, fully Qualified Domain Name
* @returns
*/
_writeRawDataMismatchRecords: function(msGr, mappedColumns, maxRowsToWrite) {
if (!msGr) {
gs.error("Failed to receive multisource glide record for populating data into workspace raw data table");
return;
}
var dataMismatchGr = new GlideRecord(this.DATA_MISMATCH);
var rowWrittenCount = 0;
while (msGr.next()) {
// read fields from multisource
var ci = msGr.getValue(this.CMDB_MULTISOURCE_DIFF_QUERY_VIEW_PREFIX_1 + this.CI);
var clazz = msGr.getValue(this.CMDB_MULTISOURCE_DIFF_QUERY_VIEW_PREFIX_1 + this.CLASS_NAME);
var source1 = msGr.getValue(this.CMDB_MULTISOURCE_DIFF_QUERY_VIEW_PREFIX_1 + this.SYS_ID);
var source2 = msGr.getValue(this.CMDB_MULTISOURCE_DIFF_QUERY_VIEW_PREFIX_2 + this.SYS_ID);
for (var index in mappedColumns) {
var columnObj = mappedColumns[index];
var leftMappedColumnValue = msGr.getValue(this.CMDB_MULTISOURCE_DIFF_QUERY_VIEW_PREFIX_1 + columnObj.indexedColumn);
var rightMappedColumnValue = msGr.getValue(this.CMDB_MULTISOURCE_DIFF_QUERY_VIEW_PREFIX_2 + columnObj.indexedColumn);
/**
* In case of OR condition, few selected attributes might have same value on both sides (left and right), and some
* attributes might have different values. DB will return a row when atleast one of the selected attributes has different value for
* OR condition, we don't want to populate raw data records for those attributes where attribute value is same( left == right).
* This situation doesn't happen for AND condition and we need to handle it explicitly
*/
if (leftMappedColumnValue && rightMappedColumnValue && leftMappedColumnValue !== rightMappedColumnValue) {
dataMismatchGr.initialize();
// populate fields in data mismatch table
dataMismatchGr.setValue(this.CLASS_NAME, clazz);
dataMismatchGr.setValue(this.CI, ci);
/**
* Group by CI (Document Id) is currently not working in Multisource dashboard,
* we need to remove this once that defect is fixed
* Populating non-cmdb Sys Id into the cmdb_reference field for grouping purpose
* And also to optimize the check for hierarchy at every row
* UI will have cmdb_reference column instead of Document Id until the defect is fixed
*/
dataMismatchGr.setValue(this.CMDB_REFERENCE, ci);
dataMismatchGr.setValue(this.DIFF_FIELD, columnObj.displayName);
dataMismatchGr.setValue(this.CLASS_1, this.MULTISOURCE_DATA);
dataMismatchGr.setValue(this.SOURCE_1, source1);
dataMismatchGr.setValue(this.SOURCE_1_VALUE, leftMappedColumnValue);
dataMismatchGr.setValue(this.CLASS_2, this.MULTISOURCE_DATA);
dataMismatchGr.setValue(this.SOURCE_2, source2);
dataMismatchGr.setValue(this.SOURCE_2_VALUE, rightMappedColumnValue);
dataMismatchGr.setValue(this.STATE, this.DRAFT);
dataMismatchGr.insert();
rowWrittenCount++;
}
}
if (rowWrittenCount >= maxRowsToWrite) {
// Wrote required number of rows to data mismatch table, avoiding further processing
return;
}
}
},
/**
*
* Calculate the indexed column, display name of the columns defined in config.
* Prepare an array of objects ( each object representing indexed column, display name and column name)
* @param {*} config
* @returns An example of return value for input config
[
{
'indexedColumn': 'col51',
'displayName': 'Fully Qualified Domain Name'
'columnName' : 'fqdn'
},
{
'indexedColumn': 'col52',
'displayName': 'RAM(MB)'
'columnName' : 'ram'
}
]
*/
_getMappedColumnsDataMismatch: function(config) {
var mappedColumnsArr = [];
var attributes = config.attributes.split(",");
var columnName2DisplayNameMap = sn_cmdb.MultiSourceAnalytics.getColumnMap(config.table);
for (var index in attributes) {
var indexedColumn = this._getIndexedColumn(config.table, attributes[index]);
var displayName = columnName2DisplayNameMap[attributes[index]];
var mappedColumns = {
'indexedColumn': indexedColumn,
'displayName': displayName,
'columnName': attributes[index]
};
mappedColumnsArr.push(mappedColumns);
}
return mappedColumnsArr;
},
/**
* Prepares an encoded query with multisource view prefixes and also replace with OR/AND based on specified condition_type
* Comma separated list of attributes are separated and indexed columns for each attribute is calculated and used in the encoded query
* @param {config} config
* @returns encoded query in string format
*/
_getEncodedDataMismatchQuery: function(config) {
var attributes = config.attributes.split(",");
var operator = (config.condition_type == 1) ? '^OR' : '^';
var encodedQuery = '';
for (var i = 0; i < attributes.length; i++) {
if (i != 0) {
encodedQuery += operator;
}
var indexedColumn = this._getIndexedColumn(config.table, attributes[i].trim());
if (indexedColumn && indexedColumn !== '') {
encodedQuery += this.CMDB_MULTISOURCE_DIFF_QUERY_VIEW_PREFIX_1 + indexedColumn + 'NSAMEAS'+ this.CMDB_MULTISOURCE_DIFF_QUERY_VIEW_PREFIX_2+ indexedColumn;
} else {
gs.error("Failed to find the indexed column for class '" + config.table + "' and attribute '" + attributes[i].trim() + "'");
return;
}
}
return encodedQuery;
},
/**
*
* @param {String} className className in CMDB hierarchy
* @param {String} columnName column defined in className of CMDB
* Iterates over all the defined mappings and finds the right indexed column used for column name
* @returns the mapping of column name to indexed column.
* Example: "fqdn"->"col12", "cpu_count"->"col13"}
*/
_getIndexedColumn: function(className, columnName) {
var columnMapping = this._getMultisourceColumnMappingByClass(className);
if (!columnMapping) {
gs.error("Failed to find the column mapping for " + className);
return;
}
return columnMapping[columnName.trim()];
},
/**
* Read the column metadata from the cmdb_multisource_column_metadata table and prepare of Map of class Name -> {attributeName-> indexedColumnName}
* Ex:- {cmdb_ci_linux_server ->{"fqdn"->"col12", "cpu_count"->"col13"}}
*/
_populateColumnMapping: function() {
var columnMapping = {};
var gr = new GlideRecord(this.MULTISOURCE_COLUMN_METADATA);
gr.query();
while (gr.next()) {
var className = gr.getValue(this.TABLE);
if (!columnMapping[className]) {
columnMapping[className] = {};
}
var indexedField = gr.getValue(this.MS_TABLE_FIELD);
var field = gr.getValue(this.FIELD);
columnMapping[className][field] = indexedField;
}
return columnMapping;
},
/**
* Calculates the mapped columns defined at all levels from the input className to cmdb, walking up the hierarchy
* @param className classname in CMDB
* @returns map of column names -> indexed columns
* Ex: {"fqdn"-> "col1", "host_name"-> "col2"}
*/
_getMultisourceColumnMappingByClass: function(className) {
var table = new GlideTableHierarchy(className);
var parents = table.getTables();
var hierarchicalColumnMappingMetadata = {};
if (!this.columnMetadataMapping) {
gs.error("Failed to find the default multisource column metadata from cache");
return;
}
for (var i = 0; i < parents.length; i++) {
gs.debug("Reading column metadata mapping for " + parents[i]);
var columnMetadataMappingByClass = this.columnMetadataMapping[parents[i]];
if (columnMetadataMappingByClass) {
for (var x in columnMetadataMappingByClass) {
// If the mapping is defined at child class, don't add the parent class mapping
if (!hierarchicalColumnMappingMetadata[x]) {
hierarchicalColumnMappingMetadata[x] = columnMetadataMappingByClass[x];
}
}
}
}
return hierarchicalColumnMappingMetadata;
},
/**
* Read the data mismatch config rows from the table and returns config objects
* @returns empty when no config is available otherwise list of config objects
*/
_getDataMismatchConfig: function() {
var configs = [];
var gr = new GlideRecord(this.DATA_MISMATCH_CONFIG);
gr.query();
while (gr.next()) {
var config = {
"table": gr.getValue(this.TABLE),
"ci_count_percent": gr.getValue(this.CI_COUNT_PERCENT),
"attributes": gr.getValue(this.ATTRIBUTES),
"condition_type": gr.getValue(this.CONDITION_TYPE),
"discovery_sources": gr.getValue(this.DISCOVERY_SOURCES)
};
configs.push(config);
}
return configs;
},
type: 'MultiSourceQueryProcessor'
};
Sys ID
8f3ac25c0f28411024b63ab1df767ef8