Untitled

 avatar
unknown
plain_text
a year ago
18 kB
7
Indexable
#!/usr/bin/env node

// ******************************** INFO **************************************
//
// Version 1.0  2024/01/23
// for Zabbix 6.0.25
//
// sync-request required
// installation:  npm install sync-request
// 
// ******************************* CONFIG *************************************

// Definition of API connection
var APIADR='http://localhost/zabbix/api_jsonrpc.php';
var APIUSR='admin';
var APIPWD='zabbix';

// The status file contains a record of created graphs - it's needed to delete graphs that become obsolete
var STATUSFILE='/opt/zabbix-discoverygraphs/zabbix-discoverygraphs.status';

// Graph color palette
var PALETTE=['69EF7B','C052E4','B6866E','145A6A','B2D097','943112','5B588F',
  'EBCECB','359721','FA6CA9','20D8FD','D07D09','4F28AF','FC2C44','738C4E',
  'A92C68','79ACFD','FA1BFC','0B5313','AC8BF8','CFDF34','62400C'];

// The name of Zabbix macro(s) that contains a JSON array of the discovery graph templates
var ZABBIXMACRO='{$DISCOVERY_GRAPH_TEMPLATES}';

// ******************************** CODE **************************************

// Generic API call. The first parameter contains an API method. The second parameter contains a data for the metod.
function APICALL (APIMETHOD,APIMETHODDATA) {

  try {
    var APIRESPONSESTRING=null;
    var APIRESPONSECODE=null;
    var APIREQUEST=require ('sync-request');
    var APIRESPONSE=APIREQUEST ('POST',APIADR,{
      json:{jsonrpc:"2.0",id:"1",auth:APIAUTH,method:APIMETHOD,params:APIMETHODDATA},
      timeout:30000
    });
    APIRESPONSESTRING=APIRESPONSE.getBody ('utf8').toString ();
    APIRESPONSECODE=APIRESPONSE.statusCode;
    if (APIRESPONSECODE!=200) {
      throw new Error ('Bad response code');
    }
    APIRESPONSE=JSON.parse (APIRESPONSESTRING).result;
    if (typeof APIRESPONSE=='undefined') {
      throw new Error ('No "result" item in API response');
    }
    return APIRESPONSE;
  } catch (E) {
    LOG (0,'!!! API HTTP request error');
    LOG (0,E.name+': '+E.message);
    LOG (0,'URL: '+APIADR);
    LOG (0,'Method: '+APIMETHOD);
    LOG (0,'Method data: '+JSON.stringify (APIMETHODDATA));
    LOG (0,'HTTP status: '+APIRESPONSECODE);
    LOG (0,'HTTP response: '+APIRESPONSESTRING);
    process.exit (1);
  }
}

// Load status file - return status object created from JSON in the status file
function LOADSTATUS () {

  try {
    var FS=require ('fs');
    var FILECONTENT=FS.readFileSync (STATUSFILE,'utf-8');
    var STAT=JSON.parse (FILECONTENT);
    return STAT;
  } catch (E) {
    LOG (0.5,'!!! WARNING: Cannot read status file \''+STATUSFILE+'\'');
    LOG (0.5,E.name+': '+E.message);
    LOG (0.5,'The obsoleting discovery graphs will not be deleted');
    LOG (0.5,'It\'s OK if the file is missing during the first run - the file will be created\n');
    return {};
  }
}


// Save status file - write status object as JSON in the status file
function SAVESTATUS (STAT) {

  try {
    var FS=require ('fs');
    FS.writeFileSync (STATUSFILE,JSON.stringify (STAT,null,4),'utf-8');
  } catch (E) {
    LOG (0.5,'!!! WARNING: Cannot write status file \''+STATUSFILE+'\'');
    LOG (0.5,E.name+': '+E.message);
    LOG (0.5,'The obsoleting discovery graphs will not be deleted during next run\n');
    return {};
  }
}

// Examnine object (host or template) if it has link to template(s) containing discovery graphs
// HOSTID = id of host the current examination is for
// OBJECTID = id of object to examnine in set of hosts or set of templates (note: if the object is host, then HOSTID=OBJECTID)
// OBJECTSET = the object set containing examined object (pass HOSTS or TEMPLATES variable here)
// PRECEDENTPATH = informational string of parent hosts/templates hierarchy
function EXAMINE (HOSTID,OBJECTID,OBJECTSET,PRECEDENTPATH) {

  // Get hierarchy path to examined object
  if (!PRECEDENTPATH) {var PATH=OBJECTSET[OBJECTID].host;}
  else {var PATH=PRECEDENTPATH+' -> '+OBJECTSET[OBJECTID].host;}

  // If object has macro containing link to discovery graph template(s) then eximanine that macro
  for (var MACROINDEX in OBJECTSET[OBJECTID].macros) {
    if (OBJECTSET[OBJECTID].macros[MACROINDEX].macro==ZABBIXMACRO) {
      EXAMINEMACRO (HOSTID,OBJECTSET[OBJECTID].macros[MACROINDEX].value,PATH);
    }
  }

  // Examine parent templates
  for (var PTEMPINDEX in OBJECTSET[OBJECTID].parentTemplates) {
    EXAMINE (HOSTID,OBJECTSET[OBJECTID].parentTemplates[PTEMPINDEX].templateid,TEMPLATES,PATH);
  }
}

// Examine string given in macro which link to discovery graph templates
// HOSTID = id of host the current examination is for
// MACROSTRING = value from the macro. Should contain JSON array of strings, each the string contains name of discovery graph template
// PATH = informational string of hosts/templates hierarchy
function EXAMINEMACRO (HOSTID,MACROSTRING,PATH) {

  var LINKEDTEMPLATES=[];
  // Parse JSON string to array
  try {
    LINKEDTEMPLATES=JSON.parse (MACROSTRING);
    if (!Array.isArray(LINKEDTEMPLATES)) {
      throw new Error ('Not an array');
    }
    if (LINKEDTEMPLATES.length==0) {
      throw new Error ('Empty array');
    }
    for (var LTEMPINDEX in LINKEDTEMPLATES) {
      if (typeof LINKEDTEMPLATES[LTEMPINDEX]!='string') {
        throw new Error ('Not an array of strings');
      }
    }
  } catch (E) {
    LOG (0.5,'!!! WARNING: Cannot parse macro value \''+MACROSTRING+'\'');
    LOG (0.5,E.name+': '+E.message);
    LOG (0.5,'Macro '+ZABBIXMACRO+' in '+PATH);
    LOG (0.5,'The macro should contain JSON array of strings, each the string contains name of discovery graph template\n');
    return;
  }

  // Save linked discovery graph templates for host
  for (var LTEMPINDEX in LINKEDTEMPLATES) {
    LINKEDTEMPLATEID=TEMPLATENAMETOID(LINKEDTEMPLATES[LTEMPINDEX]);
    if (!LINKEDTEMPLATEID) {
      LOG (0.5,'!!! WARNING: Cannot find template \''+LINKEDTEMPLATES[LTEMPINDEX]+'\'');
      LOG (0.5,'Macro: '+ZABBIXMACRO+' in '+PATH+'\n');
    }
    else {
      // If there is no record for currently examined host in HOSTSTOUPDATE then create one
      if (!HOSTSTOUPDATE[HOSTID]) {
        HOSTSTOUPDATE[HOSTID]=[];
      }
      // If the found template is not in record for currently examined host in HOSTSTOUPDATE
      // then save it
      if (!HOSTSTOUPDATE[HOSTID].find (function (T) {return T.TEMPLATEID==LINKEDTEMPLATEID;})) {
        HOSTSTOUPDATE[HOSTID].push ({TEMPLATEID:LINKEDTEMPLATEID,PATH:PATH});
        LOG (2,PATH+' ===> '+LINKEDTEMPLATES[LTEMPINDEX]);
      }
      // If the found template is in record for currently examined host in HOSTSTOUPDATE
      // then it is a duplicate link
      else {
        LOG (2,PATH+' ===> '+LINKEDTEMPLATES[LTEMPINDEX]+' (duplicity, skipping)');
      }
    }
  }
}

// Find templateid for template name. Return null if not found.
function TEMPLATENAMETOID (TEMPLATENAME) {

  for (var TEMPLATEID in TEMPLATES) {
    if (TEMPLATES[TEMPLATEID].host==TEMPLATENAME) {
      return TEMPLATEID;
    }
  }
  return null;
}

// Create/update discovery graph for host
// HOSTID = the graph will be created for this host
// GRAPHID = id of source graph from discovery graph template which will be used as a model for the created/updated graph
// TEMPLATEID = id of template which graph with GRAPHID belongs to
// PATH = informational string of hosts/templates hierarchy
function UPDATEGRAPH (HOSTID,GRAPHID,TEMPLATEID,PATH) {

  // Prepare data. Clone new graph from model graph in discovery graph template
  var HOST=HOSTS[HOSTID];
  var GRAPHMODEL=GRAPHS[GRAPHID];
  var GRAPHNAME=GRAPHMODEL.name;
  var GRAPHNEW=JSON.parse (JSON.stringify (GRAPHMODEL));
  delete GRAPHNEW.graphid;
  delete GRAPHNEW.templateid;
  delete GRAPHNEW.flags;
  delete GRAPHNEW.discover;
  delete GRAPHNEW.gitems;
  var TEMPLATE=TEMPLATES[TEMPLATEID];
  LOG (2,'* Handling graph \''+GRAPHNAME+'\' from template \''+TEMPLATE.host+'\' for host \''+HOST.host+'\' ...');

  // Check for graph name duplicity within the host
  if (STATUSNEW[HOSTID]) {
    if (STATUSNEW[HOSTID].find (function (G) {return G.NAME==GRAPHNAME;})) {
      LOG (0.5,'!!! WARNING: Graph of the same name within the host is defined in another discovery graph template');
      LOG (0.5,'Graph \''+GRAPHNAME+'\' from template \''+TEMPLATE.host+'\'');
      LOG (0.5,'for '+PATH);
      LOG (0.5,'Run with -d parameter to check debug info. Skipping the graph.\n');
      return;
    }
  }

  // Prepare items for new graph
  // Copy array of gitems from model graph
  var GITEMSMODEL=JSON.parse (JSON.stringify (GRAPHMODEL.gitems));
  var GITEMS=[];
  var COLORINDEX=0;
  var SORTORDERINDEX=0;
  // Sort the gitems array to order as the graph items are displayed
  GITEMSMODEL.sort (function (GI1,GI2) {return GI1.sortorder-GI2.sortorder;});
  // Iterate through all model graph gitems and find according items in host
  for (var GITEMMODELINDEX in GITEMSMODEL) {
    // Find item of discovery graph template corresponding to model graph's gitem
    var GITEMMODEL=GITEMSMODEL[GITEMMODELINDEX];
    var ITEMMODELID=GITEMMODEL.itemid;
    var ITEMMODEL=TEMPLATE.items.find (function (I) {return I.itemid==ITEMMODELID;});
    // If item used in model graph is not found within the model graph's template then skip the graph
    if (!ITEMMODEL) {
      LOG (0.5,'!!! WARNING: Cannot find item for discovery graph \''+GRAPHNAME+'\'');
      LOG (0.5,'The item nr. '+ITEMID+' used in the graph not found in graph\'s template:');
      LOG (0.5,PATH+' ===> '+TEMPLATE.host);
      LOG (0.5,'Skipping the graph\n');
      return;
    }
    var GITEMMODELNAME=ITEMMODEL.name;
    // Find items in the host according to model graph's item name
    var FOUNDITEMIDS=[];
    var FOUNDITEMNAMES=[];
    for (var ITEMINDEX in HOST.items) {
      if (HOST.items[ITEMINDEX].name.match (GITEMMODELNAME)) {
        FOUNDITEMIDS.push (HOST.items[ITEMINDEX].itemid);
        FOUNDITEMNAMES.push (HOST.items[ITEMINDEX].name);
      }
    }
    LOG (2,'Discovery graph item \''+GITEMMODELNAME+'\' - found these host items: \''+FOUNDITEMNAMES.join ('\', \'')+'\'');
    // Create gitems for new graph
    var FIRSTGENITEM=true;
    for (var FOUNDITEMINDEX in FOUNDITEMIDS) {
      // Copy gitem from model graph gitem to the new gitem
      var GITEM=JSON.parse (JSON.stringify (GITEMMODEL));
      delete GITEM.itemid;
      delete GITEM.graphid;
      // Choose color
      if (!FIRSTGENITEM) {
        GITEM.color=GETCOLOR (COLORINDEX);
        COLORINDEX++;
      }
      FIRSTGENITEM=false;
      // Set display order
      GITEM.sortorder=SORTORDERINDEX;
      SORTORDERINDEX++;
      // Associate the new gitem to corresponding host item
      GITEM.itemid=FOUNDITEMIDS[FOUNDITEMINDEX];
      GITEMS.push (GITEM);
    }
  }

  // Prepare new graph
  GRAPHNEW.gitems=GITEMS;
  if (GITEMS.length==0) {
    LOG (0.5,'!!! WARNING: No host items found for discovery graph');
    LOG (0.5,'Graph \''+GRAPHNAME+'\' from template \''+TEMPLATE.host+'\'');
    LOG (0.5,'for '+PATH);
    LOG (0.5,'Run with -d parameter to check debug info. Skipping the graph.\n');
    return;
  }

  // Check if graph already exist within the host
  var GRAPHCHECK=HOST.graphs.find (function (G) {return G.name==GRAPHNAME;});
  // If the graph does not exist within the host yet, it will be created
  if (!GRAPHCHECK) {
    var METHOD='graph.create';
    LOG (1,'Creating graph \''+GRAPHNAME+'\' for host \''+HOST.host+'\'');
  }
  // If the graph already exist, check if it needs to be updated
  else {
    // Compare new graph and existing graph
    var GRAPHEXISTING=GRAPHS[GRAPHCHECK.graphid];
    var GDIFFERS=false;
    for (var GRAPHNEWINDEX in GRAPHNEW) {
      if (GRAPHNEWINDEX!='gitems') {
        GDIFFERS=(GDIFFERS || (GRAPHNEW[GRAPHNEWINDEX]!=GRAPHEXISTING[GRAPHNEWINDEX]));
      }
    }
    GDIFFERS=(GDIFFERS || (GRAPHNEW.gitems.length!=GRAPHEXISTING.gitems.length))
    for (var GRAPHNEWGITEMINDEX in GRAPHNEW.gitems) {
      var GRAPHNEWGITEM=GRAPHNEW.gitems[GRAPHNEWGITEMINDEX];
      var GRAPHEXISTINGGITEM=GRAPHEXISTING.gitems.find (function (G) {return G.itemid==GRAPHNEWGITEM.itemid;});
      if (!GRAPHEXISTINGGITEM) {
        GDIFFERS=true;
      }
      else {
        for (var GRAPHNEWGITEMRECORD in GRAPHNEWGITEM) {
          GDIFFERS=(GDIFFERS || (GRAPHNEWGITEM[GRAPHNEWGITEMRECORD]!=GRAPHEXISTINGGITEM[GRAPHNEWGITEMRECORD]));
        }
      }
    }
    // If the existing and the new graph differs let update it
    if (GDIFFERS) {
      var METHOD='graph.update';
      GRAPHNEW.graphid=GRAPHEXISTING.graphid;
      LOG (1,'Updating graph \''+GRAPHNAME+'\' for host \''+HOST.host+'\'');
    }
    // If the existing and the new graph is the same let it stay as it is
    else {
      var METHOD=null;
      var NEWGRAPHID=GRAPHCHECK.graphid;
      LOG (2,'Graph not changed - no update needed');
    }
  }

  // Call API to create/update the graph for the host
  if (METHOD) {
    var NEWGRAPHID=APICALL (METHOD,GRAPHNEW).graphids[0];
  }

  // Record the graph handling into the status variable
  if (!STATUSNEW[HOST.hostid]) {
    STATUSNEW[HOST.hostid]=[];
  }
  STATUSNEW[HOST.hostid].push ({GRAPHID:NEWGRAPHID,GRAPHNAME:GRAPHNAME});
}

// Delete graph with GRAPHID from host with HOSTID
// GRAPHID = id of the graph to be deleted
// HOSTID = id of the host the graph was created for
// ORIGGRAPHNAME = name of the graph - just to print debug info in case the graph does not exist
function DELETEGRAPH (GRAPHID,HOSTID,ORIGGRAPHNAME) {

  // Check if the host exists
  var DEBUGHOSTINF='';
  var DEBUGGRAPHADJ='';
  if (HOSTS[HOSTID]) { 
    DEBUGHOSTINF=' from host \''+HOSTS[HOSTID].host+'\'';
  }
  else {
    DEBUGGRAPHADJ='orphaned ';
  }

  // Check if the graph exists. If yes, delete it
  if (GRAPHS[GRAPHID]) {
    LOG (1,'Deleting '+DEBUGGRAPHADJ+'graph \''+GRAPHS[GRAPHID].name+'\''+DEBUGHOSTINF);
    APICALL ('graph.delete',[GRAPHID]);
  }
  else {
    LOG (2,'Cannot delete '+DEBUGGRAPHADJ+'graph nr. '+GRAPHID+' \''+ORIGGRAPHNAME+'\''+DEBUGHOSTINF+' - the graph no longer exist');
  }
}

// Get color from palette
function GETCOLOR (COLORINDEX) {

  var COLOR=PALETTE[COLORINDEX];
  if (!COLOR) {COLOR='000000';};
  return COLOR;
}

// Write output depending on command line parameter
//   -d0 or -s = errors only
//   -d1 or without parameter = errors, warnings, API wOCrite
//   -d2 or -d = errors, warnings, API write, processing details
// The first parameter is severity number (according to -d parameter). The warning number is 0.5. Messages with severity below 1 go to stderr.
// The second parameter is message.
function LOG (SEVERITY,MESSAGE) {

  var REQUESTEDSEVERITY=1;
  if (process.argv[2]=='-d0') {REQUESTEDSEVERITY=0;}
  if (process.argv[2]=='-d2' || process.argv[2]=='-d') {REQUESTEDSEVERITY=2;}
  if (SEVERITY==0) {
    console.error (MESSAGE);
    return;
  }
  if (SEVERITY==0.5 && REQUESTEDSEVERITY>=1) {
    console.error (MESSAGE);
    return;
  }
  if (SEVERITY<=REQUESTEDSEVERITY) {
    console.log (MESSAGE);
  }
}

// API authentication
LOG (2,'* API authentication ...');
var APIAUTH=null;
APIAUTH=APICALL ('user.login',{user:APIUSR,password:APIPWD});

// Get all hosts
LOG (2,'* Gathering hosts ...');
HOSTS=APICALL ('host.get',{
  output:["host"],
  selectMacros:["hostmacroid","macro","value"],
  selectGraphs:["graphid","name"],
  selectItems:["itemid","name"],
  selectParentTemplates:["templateid"],
  preservekeys:true
});

// Get global macro containing discovery graph templates
LOG (2,'* Gathering global macros ...');
GLOBALMACROS=APICALL ('usermacro.get',{
  output:["globalmacroid","value"],
  search:{macro:[ZABBIXMACRO]},
  globalmacro:true
});

// Get all templates
LOG (2,'* Gathering templates ...');
TEMPLATES=APICALL ('template.get',{
  output:["host"],
  selectMacros:["hostmacroid","macro","value"],
  selectGraphs:["graphid","name"],
  selectItems:["itemid","name"],
  selectParentTemplates:["templateid"],
  preservekeys:true
});
// If global macro is present add it as virtual template and link it to all hosts
if (GLOBALMACROS[0]) {
  TEMPLATES["-1"]={
    host:"GLOBAL MACRO",
    templateid:"-1",
    parentTemplates:[],
    graphs:[],
    macros:[{
      hostmacroid:0-GLOBALMACROS[0].globalmacroid,
      macro:ZABBIXMACRO,
      value:GLOBALMACROS[0].value
    }]
  }
  for (var HOSTID in HOSTS) {
    HOSTS[HOSTID].parentTemplates.push({templateid:"-1"});
  }
}

// Get all graphs
LOG (2,'* Gathering graphs ...');
GRAPHS=APICALL ('graph.get',{
  selectGraphItems:["itemid","color","calc_fnc","drawtype","sortorder","type","yaxisside"],
  preservekeys:true
});

// Load status
LOG (2,'* Loading status file ...');
var STATUS=LOADSTATUS ();

// Get discovery graphs templates for hosts
LOG (2,'* Browsing hosts/templates topology ...');
var HOSTSTOUPDATE={};
for (var HOSTID in HOSTS) {
  EXAMINE (HOSTID,HOSTID,HOSTS,'');
}
// Print debug info of result
for (var HOSTID in HOSTSTOUPDATE) {
  var DEBUGINFO=HOSTS[HOSTID].host+' will use template(s) ';
  var FIRSTTEMPLATE=true;
  for (var TEMPLATEINDEX in HOSTSTOUPDATE[HOSTID]) {
    if (!FIRSTTEMPLATE) {
      DEBUGINFO=DEBUGINFO+', ';
    }
    DEBUGINFO=DEBUGINFO+TEMPLATES[HOSTSTOUPDATE[HOSTID][TEMPLATEINDEX].TEMPLATEID].host;
    FIRSTTEMPLATE=false;
  }
  LOG (2,DEBUGINFO);
}

// Create/update discovery graphs for hosts
var STATUSNEW={};
for (var HOSTID in HOSTSTOUPDATE) {
  for (var TEMPLATEINDEX in HOSTSTOUPDATE[HOSTID]) {
    var TEMPLATEID=HOSTSTOUPDATE[HOSTID][TEMPLATEINDEX].TEMPLATEID;
    var PATH=HOSTSTOUPDATE[HOSTID][TEMPLATEINDEX].PATH;
    if (TEMPLATES[TEMPLATEID].graphs.length==0) {
      LOG (0.5,'!!! WARNING: Discovery graph template \''+TEMPLATES[TEMPLATEID].host+'\' contains no graphs');
      LOG (0.5,'The template is used by '+PATH+'\n');
    }
    for (var GRAPHINDEX in TEMPLATES[TEMPLATEID].graphs) {
      UPDATEGRAPH (HOSTID,TEMPLATES[TEMPLATEID].graphs[GRAPHINDEX].graphid,TEMPLATEID,PATH);
    }
  }
}

// Delete obsolete discovery graphs
LOG (2,'* Checking obsolete discovery graphs ...');
for (var HOSTID in STATUS) {
  for (var GRAPHINDEX in STATUS[HOSTID]) {
    var GRAPHID=STATUS[HOSTID][GRAPHINDEX].GRAPHID;
    var GRAPHNAME=STATUS[HOSTID][GRAPHINDEX].GRAPHNAME;
    if (!STATUSNEW[HOSTID] || !STATUSNEW[HOSTID].find (function (G) {return G.GRAPHID==GRAPHID;})) {
      DELETEGRAPH (GRAPHID,HOSTID,GRAPHNAME);
    }
  }
}

// Save status
LOG (2,'* Saving status file ...');
SAVESTATUS (STATUSNEW);
Editor is loading...
Leave a Comment