├── application ├── schema │ ├── 01-migration.sql │ └── init.sql ├── img │ ├── NetworkExample.png │ └── HierarchicalExample.png ├── views │ └── scripts │ │ └── module │ │ ├── hierarchy.phtml │ │ ├── status-grid.phtml │ │ ├── welcome.phtml │ │ ├── kickstart.phtml │ │ ├── network.phtml │ │ └── settings.phtml └── controllers │ └── ModuleController.php ├── configuration.php ├── public ├── js │ ├── kickstartManager.js │ ├── errorManager.js │ ├── fullscreenManager.js │ ├── gridManager.js │ ├── requestsManager.js │ ├── settingsManager.js │ └── graphManager.js └── css │ └── module.less └── README.MD /application/schema/01-migration.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE plugin_settings ADD api_host TEXT; 2 | -------------------------------------------------------------------------------- /application/img/NetworkExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visgence/icinga2-dependency-module/HEAD/application/img/NetworkExample.png -------------------------------------------------------------------------------- /application/img/HierarchicalExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/visgence/icinga2-dependency-module/HEAD/application/img/HierarchicalExample.png -------------------------------------------------------------------------------- /application/schema/init.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE node_positions ( 2 | node_name TEXT, 3 | node_x INTEGER, 4 | node_y INTEGER 5 | ); 6 | 7 | 8 | CREATE TABLE plugin_settings ( 9 | api_endpoint TEXT, 10 | api_user TEXT, 11 | api_password TEXT, 12 | api_host TEXT 13 | ); 14 | 15 | CREATE TABLE graph_settings ( 16 | setting_name TEXT, 17 | setting_value TEXT, 18 | setting_type TEXT 19 | ); 20 | 21 | -------------------------------------------------------------------------------- /application/views/scripts/module/hierarchy.phtml: -------------------------------------------------------------------------------- 1 |
2 | tabs ?> 3 |
4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | 29 | -------------------------------------------------------------------------------- /configuration.php: -------------------------------------------------------------------------------- 1 | menuSection( 3 | N_('Dependencies') 4 | )->setUrl('dependency_plugin/module/network')->setIcon('plus' 5 | )->setRenderer(array( 6 | 'SummaryNavigationItemRenderer', 7 | 'state' => 'critical' 8 | )); 9 | 10 | $section->add(N_('Settings')) 11 | ->setUrl('dependency_plugin/module/settings'); 12 | 13 | $this->provideJsFile('fullscreenManager.js'); 14 | $this->provideJsFile('gridManager.js'); 15 | $this->provideJsFile('graphManager.js'); 16 | $this->provideJsFile('vendor/vis.min.js'); 17 | $this->provideJsFile('graphManager.js'); 18 | $this->provideJsFile('requestsManager.js'); 19 | $this->provideJsFile('kickstartManager.js'); 20 | $this->provideJsFile('errorManager.js'); 21 | $this->provideJsFile('settingsManager.js'); 22 | 23 | 24 | ?> -------------------------------------------------------------------------------- /application/views/scripts/module/status-grid.phtml: -------------------------------------------------------------------------------- 1 |
2 | tabs ?> 3 |
4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 |
16 |
17 |
18 |
19 | 20 |
21 | 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /public/js/kickstartManager.js: -------------------------------------------------------------------------------- 1 | function kickstartManager() { 2 | 3 | populateDbDropdown = (data) => { 4 | 5 | var resources = JSON.parse(data); 6 | 7 | var $dropdown = $("#resource-field"); 8 | 9 | for (i = 0; i < resources['databases'].length; i++) { 10 | $dropdown.append(""); 11 | } 12 | 13 | startFormListeners(); 14 | 15 | } 16 | 17 | processError = (error) => { 18 | 19 | errorHandler(error); 20 | 21 | } 22 | 23 | startFormListeners = () => { 24 | 25 | $('form').submit(function (data) { 26 | 27 | var formData = $("form.settings-form").serializeArray(); 28 | 29 | var settingsPromise = storeSettings(formData).then(testSettings, processError) 30 | 31 | }); 32 | 33 | } 34 | 35 | testSettings = () => { 36 | 37 | success = () => { 38 | 39 | $('#notifications').append().html('
  • Settings Saved Successfully
  • '); 40 | 41 | setTimeout(() =>{ 42 | window.location.replace('./network') 43 | }, 1000) 44 | } 45 | 46 | var hostPromise = getHosts().then(success, processError) 47 | } 48 | 49 | var resourcePromise = getIcingaResourceDatabases().then(populateDbDropdown, processError) 50 | 51 | } -------------------------------------------------------------------------------- /application/views/scripts/module/welcome.phtml: -------------------------------------------------------------------------------- 1 |
    2 | tabs ?> 3 |
    4 | 5 | 6 |
    7 |

    Welcome to The Icinga Web 2 Dependency Module

    8 | The Module Can't Seem to find any dependency or host data. Be sure hosts are added with parent vars, and that The icinga 2 apply rule is implemented. 9 | 10 |

    Creating The Apply Rule

    11 |

    An apply rule is used to generate dependency data based on host configuration files. To implement this apply rule a new conf file needs to be created in /etc/icinga2/conf.d: sudo vim /etc/icinga2/conf.d/parents.conf. Next add the folowing lines to the file:

    12 | 13 |
    apply Dependency "Parent" for (parent in host.vars.parents) to Host {
    14 |       parent_host_name = parent
    15 |       assign where host.address && host.vars.parents
    16 | } 
    17 | 
    18 | 19 |

    Format of Host Configuration Files

    20 | 21 |

    This plugin requires some configuration of stock host files in order to function.

    22 | 23 |

    Custom Variable "Parents"

    24 | 25 |

    The apply rule generates dependency data using a custom variable names "parents". this custom variable needs to be an array and can be manually inserted into host configuration files or modified using icinga director. An example host with the correct parent variable is:

    26 | 27 |
    object Host "example-host" {
    28 |     import "generic-host"
    29 | 
    30 |     display_name = "example-host"
    31 |     address = "127.0.0.1"
    32 |     check_command = "check-host-alive"
    33 |     groups = [ "homegroup" ]
    34 |     vars.parents = [ "Parent-1", "Parent-2" ]
    35 | }
    36 | 
    37 | 38 |
    39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /application/views/scripts/module/kickstart.phtml: -------------------------------------------------------------------------------- 1 |
    2 | tabs ?> 3 |
    4 | 5 | 6 |
    7 |

    Dependency Module Settings

    8 |
    9 |
    10 | Database Backend 11 |
    12 | 13 |
    Database Resource
    14 | 15 |
    16 | 18 |
    19 | 20 |
    21 |
    22 |
    23 |
    24 |
    25 | API Login Information 26 |
    27 | 28 |
    29 | 30 |
    31 |
    32 | 33 |
    34 |
    35 | 36 |
    37 |
    38 | 39 |
    40 | 41 |
    42 | 43 |
    44 |
    45 | 46 |
    47 | 48 |
    49 | 50 |
    51 |
    52 | 53 |
    54 |
    55 |
    56 |
    57 | 58 |
    59 |
    60 |
    61 |
    62 | 63 | 76 | -------------------------------------------------------------------------------- /public/js/errorManager.js: -------------------------------------------------------------------------------- 1 | function errorHandler(error) { 2 | 3 | if (error['message'] === 'setup') { 4 | $('#notifications').append().html('
  • No Configuration File Detected, Redirecting to Setup
  • '); 5 | 6 | setTimeout(()=>{ 7 | window.location.replace('./kickstart') 8 | }, 3000) 9 | 10 | } else { 11 | 12 | 13 | switch (error['type']) { 14 | case 'resources': 15 | handleResourcesError(error); 16 | break; 17 | 18 | case 'dependencies': 19 | break; 20 | 21 | case 'hosts': 22 | handleHostsError(error); 23 | break; 24 | 25 | case 'positions': 26 | handlePositionsError(error); 27 | break; 28 | 29 | case 'settings': 30 | handleSettingsError(error); 31 | break; 32 | 33 | case 'templates': 34 | handleTemplatesError(error); 35 | break; 36 | 37 | case 'nodes': 38 | handleNodes(error); 39 | break; 40 | 41 | case 'graph_settings': 42 | handleGraphSettings(error); 43 | break; 44 | 45 | case 'configuration': 46 | handleConfiguration(error); 47 | break; 48 | } 49 | 50 | } 51 | 52 | function handleSettingsError(error) { 53 | if (error['message'] === 'setup') { 54 | displayError("No Configuration File Detected, Redirecting to Setup") 55 | 56 | setTimeout(() => { 57 | window.location.replace('./kickstart') 58 | }) 59 | 60 | } else { 61 | displayError(error['message']) 62 | } 63 | } 64 | 65 | function handleHostsError(error) { 66 | 67 | 68 | displayError(error['message']) 69 | } 70 | 71 | function handleResourcesError(error) { 72 | 73 | console.log("Error Encountered While Getting Icinga Resourece: " , error); 74 | 75 | displayError(error['message']) 76 | } 77 | 78 | 79 | function handleDependenciesError(error) { 80 | 81 | displayError(error['message']) 82 | } 83 | 84 | function handleHostsError(error) { 85 | 86 | displayError(error['message']) 87 | } 88 | 89 | function handleHostsError(error) { 90 | displayError(error['message']) 91 | } 92 | 93 | function handleConfiguration(error) { 94 | 95 | displayError(error['message']) 96 | 97 | setTimeout(() => { 98 | window.location.replace('./settings') 99 | }, 5000) 100 | 101 | } 102 | 103 | function displayError(message){ 104 | 105 | if(message){ 106 | $('#notifications').append().html('
  • ' + message + '
  • '); 107 | } else { 108 | 109 | $('#notifications').append().html('
  • ' + 'Unexpected Error Encountered, Check Console' + '
  • '); 110 | } 111 | } 112 | 113 | } -------------------------------------------------------------------------------- /application/views/scripts/module/network.phtml: -------------------------------------------------------------------------------- 1 |
    2 | tabs ?> 3 |
    4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
    15 |
    16 |
    17 |
    18 |
    19 |
    20 |
    21 |
    22 | 23 |
    24 |
    25 |
    0%
    26 | 27 |
    28 | 29 |
    30 |
    31 | 32 |
    33 | 36 | 37 | 57 |
    58 | 59 |
    60 | 61 | mode_edit 62 | 63 | 64 | 85 |
    86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /application/views/scripts/module/settings.phtml: -------------------------------------------------------------------------------- 1 |
    2 | tabs ?> 3 |
    4 | 5 | 6 | 7 |
    8 |
    9 |

    Dependency Module Settings

    10 |
    11 |
    12 | 13 |
    14 |
    15 | 16 |
    17 |
    18 | Up 19 |
    20 | Down 21 |
    22 | Unreachable 23 |
    24 | Display Only Host Alias 25 |
    26 | Always Label Large Nodes 27 |
    28 |
    29 | 30 | 31 |
    32 | 33 |
    34 |
    35 | Display Only Hosts With Dependency Data 36 |
    37 | 38 |
    39 | 40 |
    41 |
    42 | Scale Nodes Based On Number Of Children 43 |
    44 | 45 |
    46 | 47 |
    48 |
    49 | Status Grid 50 |
    51 |
    52 | Network 53 |
    54 | 55 | 56 |
    57 | 58 |
    59 |
    60 | 61 |
    62 | 63 | 66 | 67 | 68 | 78 | 79 |
    80 | 81 |
    82 |
    83 |
    84 | 85 | -------------------------------------------------------------------------------- /public/js/fullscreenManager.js: -------------------------------------------------------------------------------- 1 | function fullscreenMode(container, networkData, Icinga) { 2 | 3 | hosts = []; 4 | $('.controls').hide(); 5 | $('#dependency-network').css("background-color", '#262626'); 6 | $('#dependency-network').css("height", '90%') 7 | $('#main').css("width", '100%'); 8 | $('#main').css("height", '100%'); 9 | $('#hud').css('display', 'block'); 10 | // $('#layout').addClass('fullscreen-layout'); 11 | // Icinga.ui.toggleFullscreen(); 12 | 13 | const fullscreenOptions = { 14 | 15 | layout: { 16 | improvedLayout: false, 17 | randomSeed: 728804 18 | }, 19 | edges: { 20 | smooth: { 21 | "forceDirection": "none", 22 | }, 23 | 24 | width: 5 25 | }, 26 | 27 | nodes: { 28 | borderWidth: 2, 29 | scaling: { 30 | label: true 31 | }, 32 | font: { 33 | color: 'white' 34 | }, 35 | fixed: true, 36 | shape: 'dot' 37 | 38 | } 39 | }; 40 | 41 | function success(data){ 42 | hosts = JSON.parse(data['data']) 43 | } 44 | 45 | function error(error){ 46 | errorHandler(error) 47 | 48 | throw error; 49 | } 50 | 51 | var hostPromise = getHosts(success, error).then(function () { 52 | 53 | var hostsUp = 0; 54 | var hostsDown = 0; 55 | var hostsUnreachable = 0; 56 | 57 | for (i = 0; i < hosts.results.length; i++) { 58 | 59 | var node = networkData.nodes['_data'][hosts.results[i].name]; 60 | 61 | // console.log(node); 62 | 63 | if (hosts.results[i].attrs.state === 0) { //if host is in a sate of 0 it is up, if '1' it is considered down, but can also be unreachable. 64 | color_border = 'green'; 65 | font_size = 0; 66 | hostsUp++; 67 | } else if (hosts.results[i].attrs.state === 1) { 68 | 69 | if (hosts.results[i].attrs.last_reachable === false) { 70 | color_border = 'purple'; 71 | font_size = 20; 72 | hostsUnreachable++; 73 | // problemHosts.push(hosts.results[i].name); 74 | } else { 75 | color_border = 'red'; 76 | font_size = 20; 77 | hostsDown++; 78 | // problemHosts.push(hosts.results[i].name); 79 | } 80 | } 81 | 82 | if (node != undefined) { 83 | 84 | if (node.mass > 1.5) { 85 | font_size = 20; 86 | } 87 | 88 | networkData.nodes.update({ 89 | id: hosts.results[i].name, 90 | color: { 91 | border: color_border, 92 | background: '#262626' 93 | }, 94 | font: { 95 | size: font_size, 96 | color: 'white' 97 | } 98 | }); 99 | 100 | } 101 | } 102 | 103 | var network = new vis.Network(container, networkData, fullscreenOptions); 104 | 105 | startRefreshTimeout(network); 106 | 107 | var date = new Date(); 108 | var timeUpdated = date; 109 | 110 | // console.log(timeUpdated); 111 | 112 | $('#hud-down').html("

    " + hostsDown + ' Hosts DOWN' + "

    "); 113 | $('#hud-unreachable').html('

    ' + hostsUnreachable + ' Hosts UNREACHABLE' + '

    '); 114 | $('#hud-up').html('

    ' + hostsUp + ' Hosts UP' + '

    '); 115 | $('#hud-title').html('

    ' + timeUpdated + '

    '); 116 | 117 | updateTime(); 118 | 119 | }); 120 | 121 | function updateTime() { 122 | 123 | setTimeout(() => { 124 | 125 | var date = new Date(); 126 | 127 | $('#hud-title').html('

    ' + date + '

    ') 128 | 129 | updateTime(); 130 | 131 | }, 1000); 132 | } 133 | 134 | function startRefreshTimeout(network) { 135 | 136 | setTimeout(function () { 137 | 138 | network.destroy(); 139 | 140 | 141 | getData(); 142 | 143 | }, 60000); 144 | } 145 | 146 | } -------------------------------------------------------------------------------- /public/js/gridManager.js: -------------------------------------------------------------------------------- 1 | function drawGrid() { 2 | 3 | processData = (response) => { 4 | 5 | var hosts = response['data']['results'] 6 | var nodes = new vis.DataSet([]); 7 | var edges = new vis.DataSet([]); 8 | var color_border = 'green' 9 | 10 | var hostsUp = 0 11 | var hostsDown = 0; 12 | var font_size = 0; 13 | var hostsUnreachable = 0; 14 | 15 | var level = 0; 16 | var x_pos = 1; 17 | 18 | 19 | num_cols = calculateNumCols(hosts.length); 20 | 21 | // num_cols = 17 22 | 23 | // return; 24 | 25 | 26 | if (isInFullscreenMode()) { 27 | font_color = "white" 28 | } else { 29 | font_color = "black" 30 | } 31 | 32 | for (i = 0; i < hosts.length; i++) { 33 | 34 | if (hosts[i]['attrs'].state === 0) { 35 | color_border = 'green'; 36 | color_background = 'rgba(0, 204, 3, 0.25)' 37 | font_size = 0; 38 | hostsUp++; 39 | } else if (hosts[i]['attrs'].state === 1 && !hosts[i]['attrs'].last_reachable) { 40 | font_size = 20; 41 | color_border = "purple"; 42 | color_background = 'rgba(98, 0, 178, 0.25)' 43 | hostsUnreachable++; 44 | 45 | } else { 46 | font_size = 20; 47 | color_border = "red"; 48 | color_background = 'rgba(204, 0, 0, 0.25)' 49 | hostsDown++; 50 | } 51 | 52 | if (i % num_cols === 0) { 53 | level = level + 1; 54 | x_pos = 0 55 | } else { 56 | x_pos += 1; 57 | } 58 | 59 | nodes.update({ 60 | id: hosts[i].name, 61 | level: level, 62 | borderWidth: 2, 63 | label: hosts[i].name, 64 | x: x_pos * 205, 65 | y: level * 205, 66 | color: { 67 | border: color_border, 68 | background: color_background 69 | }, 70 | 71 | font: { 72 | size: font_size, 73 | color: font_color, 74 | vadjust: -150 75 | }, 76 | 77 | }); 78 | } 79 | 80 | var networkData = { 81 | nodes: nodes, 82 | edges: edges 83 | }; 84 | 85 | var container = document.getElementById('grid-container'); 86 | 87 | const networkOptions = { 88 | nodes: { 89 | shape: 'square', 90 | fixed: true, 91 | size: 100, 92 | scaling: { 93 | min: 1, 94 | max: 15, 95 | label: { 96 | enabled: false, 97 | min: 14, 98 | max: 30, 99 | maxVisible: 30, 100 | drawThreshold: 5 101 | }, 102 | 103 | }, 104 | } 105 | }; 106 | 107 | var network = new vis.Network(container, networkData, networkOptions); 108 | 109 | if (isInFullscreenMode()) { 110 | 111 | fullscreenMode(hosts, network, hostsDown, hostsUp, hostsUnreachable) 112 | 113 | } else { 114 | startListeners(network) 115 | } 116 | 117 | } 118 | 119 | processError = (error) => { 120 | // errors[error['type']] = error['data'] 121 | throw error; 122 | } 123 | 124 | var hostPromise = getHosts().then(processData, processError) 125 | 126 | function startListeners(network) { 127 | 128 | network.on("click", function (params) { //double click on node listener 129 | if (params.nodes[0] != undefined) { 130 | 131 | let hostMonitoringAddress = ''; 132 | 133 | if (location.href.indexOf('/icingaweb2') > 1) { 134 | 135 | hostMonitoringAddress = 'icingaweb2/monitoring/host/show?host=' 136 | } else { 137 | 138 | hostMonitoringAddress = 'monitoring/host/show?host='; 139 | } 140 | 141 | location.href = './statusGrid#!/' + hostMonitoringAddress + params.nodes[0]; //redirect to host info page. 142 | } 143 | }); 144 | 145 | network.on('resize', (params) => { 146 | 147 | isBeingDestroyed = (params.width === 0 && params.height === 0) 148 | 149 | if (!isBeingDestroyed) { 150 | drawGrid() 151 | network.off(); 152 | network.destroy(); 153 | } else { 154 | network.off(); 155 | network.destroy(); 156 | } 157 | }) 158 | } 159 | 160 | function calculatePercentage(num, total) { 161 | 162 | return " (" + Math.round((num / total) * 100) + "%) " 163 | 164 | } 165 | 166 | function updateTime() { 167 | 168 | setTimeout(() => { 169 | 170 | var date = new Date(); 171 | $('#hud-title').html('

    ' + date + '

    ') 172 | 173 | updateTime(); 174 | 175 | }, 1000); 176 | } 177 | 178 | function startRefreshTimeout(network) { 179 | 180 | setTimeout(function () { 181 | 182 | network.destroy(); 183 | 184 | drawGrid(); 185 | 186 | }, 60000); 187 | } 188 | 189 | function fullscreenMode(hosts, network, hostsDown, hostsUp, hostsUnreachable) { 190 | 191 | updateTime(); 192 | var date = new Date(); 193 | var timeUpdated = date; 194 | 195 | $('#hud-down').html("

    " + hostsDown + calculatePercentage(hostsDown, hosts.length) + ' Hosts DOWN' + "

    "); 196 | $('#hud-unreachable').html('

    ' + hostsUnreachable + calculatePercentage(hostsUnreachable, hosts.length) + ' Hosts UNREACHABLE' + '

    '); 197 | $('#hud-up').html('

    ' + hostsUp + calculatePercentage(hostsUp, hosts.length) + ' Hosts UP' + '

    '); 198 | $('#hud-title').html('

    ' + timeUpdated + '

    '); 199 | 200 | 201 | $('.controls').hide(); 202 | $('#grid-container').css("background-color", '#262626'); 203 | $('#grid-container').css("height", '90%') 204 | $('#main').css("width", '100%'); 205 | $('#main').css("height", '100%'); 206 | $('#hud').css('display', 'block'); 207 | 208 | startRefreshTimeout(network); 209 | 210 | } 211 | 212 | function isInFullscreenMode() { 213 | return (window.location.href.indexOf('Fullscreen') > -1) 214 | } 215 | 216 | function isInMonitoringMode() { 217 | return (window.location.href.indexOf('monitoring') > -1) 218 | } 219 | 220 | function calculateNumCols(numHosts) { 221 | 222 | let screenRatio = Math.round(($('#grid-container').innerWidth() / $('#grid-container').innerHeight()) * 10) / 10 223 | 224 | 225 | for (i = 0; i < numHosts; i++) { 226 | for (y = 0; y < numHosts; y++) { 227 | ratio = Math.round((y / i) * 10) / 10 228 | total = Math.round(i * y) 229 | if (ratio === screenRatio) { 230 | if (total >= numHosts) { 231 | return (y) 232 | } 233 | } 234 | } 235 | } 236 | } 237 | } -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | Visgence Inc. 2017 2 | 3 | This program is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation, either version 3 of the License, or 6 | (at your option) any later version. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU General Public License for more details. 12 | 13 | You should have received a copy of the GNU General Public License 14 | along with this program. If not, see 15 | # Icinga 2 Dependency Module 16 | ### An Icinga Web 2 Module for mapping network topology and displaying realtime status of Icinga 2 Networks 17 | ![alt tag](application/img/NetworkExample.png) 18 | ![alt tag](application/img/HierarchicalExample.png) 19 | 20 | ## Introduction 21 | This module implements vis.js to create a topological network map for Icinga 2 Web. To generate the relationships between the nodes, a custom variable "parents" is used in the host configurtion files, similar to use in Icinga 1/Nagios. To generate depedency information, an apply rule is applied which creates up to date depenency information, including multiple parentage. 22 | 23 | ## Features 24 | 1. Displays Icinga 2 network topology in either hierarchical or network layouts. 25 | 2. Displays up-to-date status information for hosts based on state information pulled from Icinga 2 API 26 | 3. Allows for customization of network topology after initial simulation via dragging nodes to desired locations 27 | 4. Automatically redraws network when a host is added or removed. 28 | 5. Directly links to host information via double clicking on nodes. 29 | 6. Display networks in fullscreen while updateing graph realtime. 30 | 31 | # Installation 32 | These instructions assume all listed operations are being performed on a machine that has icinga 2 and icinga web 2 installed, including a database system such as MySQL or Postgresql. 33 | 34 | First clone this repository into `/usr/share/icingaweb2/modules` 35 | Next rename to dependency_plugin: `mv icinga2-dependency-module dependency_plugin` 36 | 37 | Create directory `/etc/icingaweb2/modules/dependency_plugin/` 38 | In that directory create file `config.ini` and add the following: 39 | ``` 40 | [db] 41 | resource = "dependencies" 42 | ``` 43 | 44 | ## Icinga 2 Dependencies 45 | This module will attempt to graph ***any*** dependencies, an example apply rule to generate dependencies is shown below (create new file /etc/icinga2/conf.d/parents.conf): 46 | ``` 47 | apply Dependency "Parent" for (parent in host.vars.parents) to Host { 48 | parent_host_name = parent 49 | assign where host.address && host.vars.parents 50 | } 51 | ``` 52 | this apply rule generates a dependency between hosts using a custom variable "parents". 53 | 54 | 55 | ### Custom Variable "Parents" 56 | To use the example apply rule to generate dependencies, insert the following into host configuration files or modify using icinga director. An example host with the correct parent variable is: 57 | ``` 58 | object Host "example-host" { 59 | import "generic-host" 60 | 61 | display_name = "example-host" 62 | address = "127.0.0.1" 63 | check_command = "check-host-alive" 64 | groups = [ "homegroup" ] 65 | vars.parents = [ "Parent-1", "Parent-2" ] 66 | } 67 | ``` 68 | ***Note: The apply rule will fail if hosts listed in vars.parents do not exist, ensure that the parents exist before attempting to deploy.*** 69 | 70 | 71 | ## Create Database And Users 72 | Both Postgresql and MySQL are supported. 73 | 74 | ### Setup db and User 75 | 76 | ### Using Postgresql 77 | With access to psql: 78 | 79 | ``` 80 | psql -q -c "CREATE DATABASE dependencies WITH ENCODING 'UTF8';" 81 | psql dependencies -q -c "CREATE USER dependencies WITH PASSWORD 'dependencies'; 82 | GRANT ALL PRIVILEGES ON DATABASE dependencies TO dependencies;" 83 | ``` 84 | ***Note: replace password "dependencies" with desired password*** 85 | 86 | 87 | 88 | #### Depending on postgres configuration, this may be neccesary if "ident" errors are encountered during setup: 89 | add the following to /var/lib/pgsql/data/pg_hba.conf (CentOS) 90 | or /etc/postgresql/***Postgres Version***/main/pg_hba.conf (Debian) 91 | 92 | 93 | ``` 94 | local dependencies dependencies md5 95 | host dependencies dependencies 127.0.0.1/32 md5 96 | host dependencies dependencies ::1/128 md5 97 | ``` 98 | and restart postgres `systemctl restart postgresql` 99 | 100 | 101 | #### Create Schema 102 | 103 | `psql dependencies -d dependencies < /usr/share/icingaweb2/modules/dependency_plugin/application/schema/init.sql` 104 | 105 | ### Altnernatively, using MySQL 106 | ``` 107 | mysql -e "CREATE DATABASE dependencies CHARACTER SET 'utf8'; 108 | GRANT ALL ON dependencies.* TO dependencies@localhost IDENTIFIED BY 'dependencies';" 109 | ``` 110 | ***Note: replace password "dependencies" with desired password*** 111 | 112 | #### Create Schema 113 | ``` 114 | mysql -U dependencies -D dependencies < /usr/share/icingaweb2/modules/dependency_plugin/application/schema/init.sql 115 | ``` 116 | 117 | ### Create Icinga 2 API user 118 | In order to query data, this module needs full API access to Icinga Web 2. This can be accomplished by adding a new user to api-users.conf file 119 | using `sudo vim /etc/icinga2/conf.d/api-users.conf` add the following lines to the configuration file: 120 | ``` 121 | object ApiUser "dependencies" { 122 | password = "dependencies" 123 | permissions = [ "status/query", "objects/query/*" ] 124 | } 125 | ``` 126 | ***Note: replace password "dependencies" with desired password*** 127 | 128 | ### Enable Module 129 | In Icinga Web, navigate to Configuration > Modules, and select dependency plugin. Next to "State" click enable. 130 | 131 | ### Add Created Database to Icinga 2 Application Resources 132 | 1. In Icinga Web 2 select configuration menu item. 133 | 2. Select Application entry. 134 | 3. Select Resource Tab. 135 | 3. Select create new resource. 136 | 4. Create new resosurce using filling in resource name, database name, username, and password. (default is dependencies for all) 137 | 5. Validate resource and save changes. 138 | 6. Refresh Icinga 2 Page 139 | 140 | 141 | ### Launch Kickstart Page 142 | Next, navigate to the modules entry in Icinga Web 2 to automatically launch the kickstart page and finish the setup process by selecting created resource and entering in Icinga 2 API information. The icinga2 default API port is 5665. If you have misconfigured the settings and maybe getting an `Unexpected Error Encountered, Check Console` you may always call /dependency_plugin/module/kickstart again to correct the settings again. 143 | 144 | ### Add Custom Data Field 145 | Navigate to Icinga Director > Define Data Fields. Add new entry with Caption and Field name both equal to 'parents', Data type is Array, click Add. 146 | 147 | ### Update or Add Template with new Data Field 148 | Navigate to Icinga Director > Hosts > Host Templates. Choose one to modify or add new. 149 | Modify: Choose Template, click Fields tab, choose parents from drop down list and click Add. 150 | Add: Enter name and other data then click Add. New tabs appear at the top right, click Fields tab, choose 'parents' from drop down list and click Add. 151 | 152 | # Upgrade 153 | 154 | Apply all files ending with `-migration.sql` which are in `application/schema` in ascending 155 | order. 156 | -------------------------------------------------------------------------------- /public/js/requestsManager.js: -------------------------------------------------------------------------------- 1 | function calculateRequestURL() { 2 | // checks if /icingaweb2 is present in routing or has been removed, neccessary for hitting correct endpoints 3 | 4 | if (location.href.indexOf('/icingaweb2') > 1) { 5 | return ('/icingaweb2/dependency_plugin/module/'); 6 | } else { 7 | return ('/dependency_plugin/module/'); 8 | } 9 | } 10 | 11 | 12 | function getIcingaResourceDatabases() { 13 | 14 | 15 | let requestURL = calculateRequestURL() + 'getResources'; 16 | 17 | var promise = new Promise((resolve, reject) => { 18 | 19 | 20 | $.ajax({ 21 | url: requestURL, //get Icinga Resource List 22 | type: 'GET', 23 | success: (response) => { 24 | resolve(response); 25 | }, 26 | error: (error) => { 27 | 28 | if (error['responseJSON']) { 29 | reject({ 30 | 'type': 'resources', 31 | 'message': error['responseJSON']['message'], 32 | 'code': error.code 33 | }); 34 | } else { //non standard error 35 | reject({ 36 | 'type': 'resources', 37 | 'message': error, 38 | 'code': error.code 39 | }) 40 | } 41 | } 42 | }); 43 | }); 44 | 45 | return promise; 46 | 47 | } 48 | 49 | function getDependencies() { 50 | 51 | let requestURL = calculateRequestURL() + 'getDependency' 52 | 53 | 54 | var promise = new Promise((resolve, reject) => { 55 | 56 | $.ajax({ 57 | url: requestURL, //get dependencies 58 | type: 'GET', 59 | success: (data) => { 60 | 61 | dependencies = (JSON.parse(data)); 62 | resolve({ 63 | type: 'dependencies', 64 | data: dependencies, 65 | }); 66 | }, 67 | error: (error) => { 68 | reject({ 69 | 'type': 'dependencies', 70 | 'message': error['responseJSON']['message'], 71 | 'code': error.code, 72 | }); 73 | } 74 | 75 | }); 76 | 77 | }); 78 | 79 | return promise; 80 | 81 | } 82 | 83 | function getHosts() { 84 | 85 | 86 | let requestURL = calculateRequestURL() + 'getHosts'; 87 | 88 | var promise = new Promise((resolve, reject) => { 89 | 90 | $.ajax({ 91 | url: requestURL, //get host states 92 | type: 'GET', 93 | success: function (hostData) { 94 | 95 | hosts = (JSON.parse(hostData)); 96 | resolve({ 97 | type: 'hosts', 98 | data: hosts 99 | }); 100 | }, 101 | error: (error) => { 102 | console.log(error) 103 | reject({ 104 | 'type': 'hosts', 105 | 'message': error['responseJSON']['message'], 106 | 'code': error['code'] 107 | }); 108 | } 109 | }); 110 | 111 | }); 112 | 113 | return promise; 114 | 115 | } 116 | 117 | function getNodePositions() { 118 | 119 | let requestURL = calculateRequestURL() + 'getNodes'; 120 | 121 | var promise = new Promise((resolve, reject) => { 122 | $.ajax({ 123 | url: requestURL, //get node positions 124 | type: 'GET', 125 | success: (data) => { 126 | data = JSON.parse(data); 127 | if (data === "EMPTY!") { 128 | resolve({ 129 | 'type': 'positions', 130 | 'data': null 131 | }); 132 | } else { 133 | resolve({ 134 | 'type': 'positions', 135 | 'data': data 136 | }); 137 | } 138 | 139 | }, 140 | error: (error) => { 141 | reject({ 142 | 'type': 'positions', 143 | 'message': error['responseJSON']['message'], 144 | 'code': error['code'] 145 | }); 146 | } 147 | }); 148 | }); 149 | 150 | return promise; 151 | 152 | } 153 | 154 | function getSettings() { 155 | 156 | let requestURL = calculateRequestURL() + 'getGraphSettings'; 157 | 158 | var promise = new Promise((resolve, reject) => { 159 | 160 | 161 | $.ajax({ 162 | url: requestURL, //get host states 163 | type: 'GET', 164 | success: function (data) { 165 | 166 | settings = JSON.parse(data); 167 | 168 | resolve({ 169 | 'type': 'settings', 170 | 'data': settings 171 | }); 172 | }, 173 | error: (error) => { 174 | console.log(error); 175 | reject({ 176 | 'type': 'settings', 177 | 'message': error['responseJSON']['message'], 178 | 'code': error['code'] 179 | }); 180 | } 181 | 182 | }); 183 | 184 | }); 185 | 186 | return promise; 187 | 188 | } 189 | 190 | // function getTemplates() { 191 | 192 | // let requestURL = calculateRequestURL() + 'templates'; 193 | 194 | // var promise = new Promise((resolve, reject) => { 195 | 196 | // $.ajax({ 197 | // url: requestURL, 198 | // type: 'GET', 199 | // headers: { 200 | // 'Accept': 'application/json' 201 | // }, 202 | // success: (data) => { 203 | // resolve(data); 204 | // }, 205 | // error: (data) => { 206 | 207 | // reject({ 208 | // 'type': 'templates', 209 | // 'message': error['responseJSON']['message'], 210 | // 'code': error[code] 211 | // }); 212 | 213 | // } 214 | 215 | // }); 216 | 217 | // }); 218 | 219 | // return promise; 220 | 221 | // } 222 | 223 | function storeNodePositions(data) { 224 | 225 | let requestURL = calculateRequestURL() + 'storeNodePositions'; 226 | 227 | 228 | var promise = new Promise((resolve, reject) => { 229 | $.ajax({ 230 | url: requestURL, 231 | type: 'POST', 232 | data: { 233 | json: JSON.stringify(data) 234 | }, 235 | success: () => { 236 | resolve(); 237 | }, 238 | error: (error) => { 239 | reject({ 240 | 'type': 'nodes', 241 | 'message': error['responseJSON']['message'], 242 | 'code': error['code'] 243 | }); 244 | } 245 | }); 246 | }) 247 | 248 | return promise; 249 | 250 | } 251 | 252 | // function storeNodePositions(data) { 253 | 254 | // var promise = new Promise((resolve, reject) => { 255 | // $.ajax({ //ajax request to store into DB 256 | // url: "./storeNodePositions", 257 | // type: 'POST', 258 | // data: { 259 | // json: JSON.stringify(data) 260 | // }, 261 | // success: () => { 262 | // resolve(); 263 | // }, 264 | // error: (error) => { 265 | // reject({ 266 | // 'type': 'positions', 267 | // 'message': error['responseJSON']['message'], 268 | // 'code': error['code'] 269 | // }); 270 | // } 271 | // }); 272 | 273 | // }); 274 | // return promise; 275 | // } 276 | 277 | function storeSettings(settings) { 278 | 279 | let requestURL = calculateRequestURL() + 'storeSettings'; 280 | 281 | var promise = new Promise((resolve, reject) => { 282 | 283 | payload = JSON.stringify(settings) 284 | 285 | // console.log(payload) 286 | 287 | $.ajax({ 288 | url: requestURL, 289 | type: 'POST', 290 | data: { 291 | json: payload 292 | }, 293 | success: () => { 294 | resolve(); 295 | }, 296 | error: (error) => { 297 | reject({ 298 | 'type': 'settings', 299 | 'message': error['responseJSON']['message'], 300 | 'code': error['code'] 301 | }); 302 | } 303 | }); 304 | }); 305 | 306 | return promise; 307 | } 308 | 309 | function storeGraphSettings(settings) { 310 | 311 | let requestURL = calculateRequestURL() + 'storeGraphSettings'; 312 | 313 | var promise = new Promise((resolve, reject) => { 314 | 315 | payload = JSON.stringify(settings) 316 | 317 | $.ajax({ 318 | url: requestURL, 319 | type: 'POST', 320 | data: { 321 | json: payload 322 | }, 323 | success: () => { 324 | resolve(); 325 | }, 326 | error: (error) => { 327 | reject({ 328 | 'type': 'graph_settings', 329 | 'message': error['responseJSON']['message'], 330 | 'code': error['code'] 331 | }); 332 | } 333 | }); 334 | }); 335 | 336 | return promise; 337 | } -------------------------------------------------------------------------------- /public/css/module.less: -------------------------------------------------------------------------------- 1 | 2 | /*Main Pages*********************************************************/ 3 | #dependency-network { 4 | width: 100%; 5 | height: 100%; 6 | float: left; 7 | line-height: 0; 8 | } 9 | 10 | #grid-container{ 11 | width: 100%; 12 | height: 100%; 13 | float: left; 14 | } 15 | 16 | 17 | 18 | #edit-fabs { 19 | position: fixed; 20 | bottom: 1em; 21 | right: 0em; 22 | padding-right: 1em; 23 | height: 8em; 24 | } 25 | 26 | #dependency-fabs { 27 | position: fixed; 28 | display: none; 29 | bottom: 1em; 30 | padding-left: 1em; 31 | height: 8em; 32 | } 33 | 34 | #dependency-fabs-menu{ 35 | position: absolute; 36 | right: auto; 37 | left: 6em; 38 | top: 20%; 39 | transform: translateY(-40%); 40 | width: 9.5em; 41 | list-style: none; 42 | text-align: right; 43 | margin: 0; 44 | padding-top: 1em; 45 | padding-left: 0; 46 | } 47 | 48 | #edit-fabs-menu { 49 | position: absolute; 50 | right: 6em; 51 | left: auto; 52 | top: 45%; 53 | transform: translateY(-40%); 54 | height: 100%; 55 | width: 25em; 56 | list-style: none; 57 | text-align: right; 58 | margin: 0; 59 | padding-top: 0.6em; 60 | } 61 | 62 | .zoom-fab { 63 | display: inline-block; 64 | width: 40px; 65 | height: 40px; 66 | padding-top: 5px; 67 | line-height: 40px; 68 | border-radius: 50%; 69 | background-color: @icinga-blue; 70 | vertical-align: middle; 71 | text-decoration: none; 72 | text-align: center; 73 | transition: 0.2s ease-out; 74 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); 75 | cursor: pointer; // color: #FFF; 76 | } 77 | 78 | .zoom-fab:hover { 79 | background-color: #4db6ac; 80 | box-shadow: 0 3px 3px 0 rgba(0, 0, 0, 0.14), 0 1px 7px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -1px rgba(0, 0, 0, 0.2); 81 | } 82 | 83 | .fab-btn-large { 84 | width: 60px; 85 | height: 60px; 86 | line-height: 60px; 87 | padding-top: 10px; 88 | } 89 | 90 | .material-icons { 91 | color: white; 92 | font-size: 30px; 93 | } 94 | 95 | .fab-menu li { 96 | display: inline-block; 97 | margin-right: 10px; 98 | background: transparent; 99 | } 100 | 101 | // .scale-transition { 102 | // transition: transform 0.3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important; 103 | // } 104 | 105 | .scale-transition.scale-out { 106 | transform: scale(0); 107 | transition: transform 0.2s !important; 108 | } 109 | // .scale-transition.scale-in { 110 | // transform: scale(1); 111 | // } 112 | 113 | #loadingBar { 114 | position: absolute; 115 | top: 0px; 116 | display: none; // left:10%; 117 | width: 100%; 118 | height: 100%; 119 | float: left; 120 | background-color: rgba(200, 200, 200, 0.8); 121 | -webkit-transition: all 0.5s ease; 122 | -moz-transition: all 0.5s ease; 123 | -ms-transition: all 0.5s ease; 124 | -o-transition: all 0.5s ease; 125 | transition: all 0.5s ease; 126 | opacity: 1; 127 | } 128 | 129 | #text { 130 | position: absolute; 131 | top: 0px; 132 | left: 530px; 133 | width: 30px; 134 | height: 50px; 135 | margin: auto auto auto auto; 136 | font-size: 22px; 137 | color: #000000; 138 | } 139 | 140 | div.outerBorder { 141 | position: relative; 142 | top: 400px; 143 | width: 600px; 144 | height: 44px; 145 | margin: auto auto auto auto; 146 | border: 8px solid rgba(0, 0, 0, 0.1); 147 | background: rgb(252, 252, 252); 148 | /* Old browsers */ 149 | background: -moz-linear-gradient(top, rgba(252, 252, 252, 1) 0%, rgba(237, 237, 237, 1) 100%); 150 | /* FF3.6+ */ 151 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(252, 252, 252, 1)), color-stop(100%, rgba(237, 237, 237, 1))); 152 | /* Chrome,Safari4+ */ 153 | background: -webkit-linear-gradient(top, rgba(252, 252, 252, 1) 0%, rgba(237, 237, 237, 1) 100%); 154 | /* Chrome10+,Safari5.1+ */ 155 | background: -o-linear-gradient(top, rgba(252, 252, 252, 1) 0%, rgba(237, 237, 237, 1) 100%); 156 | /* Opera 11.10+ */ 157 | background: -ms-linear-gradient(top, rgba(252, 252, 252, 1) 0%, rgba(237, 237, 237, 1) 100%); 158 | /* IE10+ */ 159 | background: linear-gradient(to bottom, rgba(252, 252, 252, 1) 0%, rgba(237, 237, 237, 1) 100%); 160 | /* W3C */ 161 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fcfcfc', endColorstr='#ededed', GradientType=0); 162 | /* IE6-9 */ 163 | border-radius: 72px; 164 | box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2); 165 | } 166 | 167 | #bar { 168 | position: absolute; 169 | top: 5px; 170 | left: 0px; 171 | width: 20px; 172 | height: 20px; 173 | margin: auto auto auto auto; 174 | border-radius: 11px; 175 | border: 2px solid rgba(30, 30, 30, 0.05); 176 | background: rgb(0, 173, 246); 177 | /* Old browsers */ 178 | box-shadow: 2px 0px 4px rgba(0, 0, 0, 0.4); 179 | } 180 | 181 | 182 | 183 | /*Fulscreen HUD******************************************************/ 184 | #hud { 185 | height: 10%; 186 | display: none; 187 | background: #383838; 188 | .hud-box { 189 | display: block; 190 | float: left; // text-decoration: underline; 191 | margin: 0.5% 0.5% 0.5% 0.5%; 192 | font-size: large; 193 | text-align: center; 194 | color: white; 195 | height: 73%; 196 | width: 24%; 197 | } 198 | .hud-title-box { 199 | display: block; 200 | float: left; // text-decoration: underline; 201 | margin: 0.5% 0.5% 0.5% 0.5%; 202 | font-size: large; 203 | text-align: center; 204 | color: white; 205 | height: 73%; 206 | width: 24%; 207 | } 208 | #hud-title { 209 | h1 { 210 | font-weight: 600; 211 | font-size: 1.15vw; 212 | margin: 1.15em 0 0.333em; 213 | border: none; 214 | } 215 | background: grey; 216 | text-decoration: none; 217 | } 218 | #hud-down { 219 | h1 { 220 | font-weight: 600; 221 | font-size: 1.25vw; 222 | margin: 1em 0 0.333em; 223 | border: none; 224 | } 225 | background: red; 226 | } 227 | #hud-unreachable { 228 | h1 { 229 | font-weight: 600; 230 | font-size: 1.25vw; 231 | margin: 1em 0 0.333em; 232 | border: none; 233 | } 234 | background: purple; 235 | } 236 | #hud-up { 237 | h1 { 238 | font-weight: 600; 239 | font-size: 1.25vw; 240 | margin: 1em 0 0.333em; 241 | border: none; 242 | } 243 | background: green; 244 | } 245 | } 246 | 247 | 248 | /*Settings Page*****************************************************/ 249 | #module-settings { 250 | max-width: 20%; 251 | padding: 10px; 252 | float: left; 253 | } 254 | 255 | #network-preview { 256 | width: 80%; 257 | height: 100%; 258 | float: right; 259 | border: 1px solid lightgray; 260 | line-height: 0; 261 | } 262 | 263 | #settings-form { 264 | h1 { 265 | border-bottom: 1px solid #eee; 266 | font-size: 1.333em; 267 | font-weight: 600; 268 | margin: 0.556em 0 1em 1em; 269 | } 270 | fieldset { 271 | border: 0px; 272 | legend { 273 | margin: 0em 0 0.5em 0.5em; 274 | font-size: 1em; 275 | border-bottom: 1px solid #c9c9c9; 276 | font-weight: bold; 277 | display: block; 278 | width: 100%; // padding-left: 1em; 279 | line-height: 2em; 280 | cursor: pointer; 281 | } 282 | dt { 283 | padding: 0.5em 0.5em; 284 | } 285 | dd { 286 | input { 287 | vertical-align: middle; 288 | /* margin-left: 27px; */ 289 | bottom: 1px; 290 | left: -2px; 291 | position: relative; // width: 15px; 292 | } 293 | margin: 0; 294 | padding: 0.3em 0.5em; 295 | } 296 | } 297 | } 298 | 299 | /*Welcome Page***************************************************/ 300 | #welcome-page { 301 | padding: 10px; 302 | color: #666; 303 | } 304 | 305 | /*Kickstart Page********************************************************/ 306 | div.dependency-module-form { 307 | h1 { 308 | margin: 0.556em 0 1em 1em; 309 | } 310 | label { 311 | width: auto; 312 | font-weight: normal; 313 | font-size: inherit; 314 | } 315 | fieldset { 316 | min-width: 36em; 317 | border: 0px; 318 | legend { 319 | margin: 0em 0 0.5em 0.5em; 320 | font-size: 1em; 321 | border-bottom: 1px solid #c9c9c9; 322 | font-weight: bold; 323 | display: block; 324 | width: 100%; // padding-left: 1em; 325 | line-height: 2em; 326 | } 327 | dt { 328 | display: inline-block; 329 | vertical-align: top; 330 | min-width: 12em; 331 | min-height: 2.5em; 332 | width: 30%; 333 | padding: 1em 0.5em; 334 | margin: 0; 335 | } 336 | dd { 337 | select, 338 | input[type=text], input[type=password] { 339 | max-width: 36em; 340 | width: 100%; 341 | line-height: 2em; 342 | height: 2.4em; 343 | padding-left: 0.5em; 344 | border-style: solid; 345 | border-color: transparent; 346 | border-bottom-color: #eee; // border: 1px solid #ddd; 347 | cursor: pointer; 348 | background: none; 349 | border-width: 1px 1px 1px 3px; 350 | background-color: inherit; 351 | } 352 | display: inline-block; 353 | width: 63%; 354 | min-height: 2.5em; 355 | vertical-align: top; 356 | margin: 0; 357 | padding: 0.3em 0.5em; 358 | } 359 | } 360 | } 361 | 362 | 363 | 364 | -------------------------------------------------------------------------------- /public/js/settingsManager.js: -------------------------------------------------------------------------------- 1 | function settingsManager() { 2 | 3 | loadSaved(); 4 | 5 | 6 | $("#settings-form").change(() => { 7 | drawPreviewNetwork(); 8 | }); 9 | 10 | $('#submit-button').click(() => { 11 | saveSettings(); 12 | }); 13 | 14 | } 15 | 16 | function loadSettings() { 17 | 18 | var moduleSettings = { 19 | 20 | 'default_dependency_template': '', 21 | 22 | 'enable_director': false, 23 | 24 | 'display_up': true, 25 | 26 | 'display_down': true, 27 | 28 | 'display_unreachable': true, 29 | 30 | 'display_only_dependencies': false, 31 | 32 | 'scaling': true, 33 | 34 | 'alias_only': true, 35 | 36 | 'fulscreen_mode': 'network', 37 | 38 | 'label_large_nodes': true, 39 | 40 | 'text_size': 50 41 | } 42 | 43 | 44 | moduleSettings.display_only_dependencies = $("#host-mode-checkbox").prop('checked'); 45 | moduleSettings.display_up = $("#node-text-up-checkbox").prop('checked'); 46 | moduleSettings.display_down = $("#node-text-down-checkbox").prop('checked'); 47 | moduleSettings.display_unreachable = $("#node-text-unreachable-checkbox").prop('checked'); 48 | moduleSettings.scaling = $("#scaling-mode-checkbox").prop('checked'); 49 | moduleSettings.text_size = $("#text-size-range").val() / 2; 50 | moduleSettings.label_large_nodes = $('#label-mode-checkbox').prop('checked'); 51 | moduleSettings.alias_only = $('#alias-label-checkbox').prop('checked'); 52 | // moduleSettings.default_dependency_template = $("#dependency-template-field").val(); 53 | moduleSettings.enable_director = $("#director-checkbox").prop('checked'); 54 | if ($('#fullscreen-grid-checkbox').prop('checked')){ 55 | moduleSettings.fullscreen_mode = 'grid' 56 | } 57 | if ($('#fullscreen-network-checkbox').prop('checked')) { 58 | moduleSettings.fullscreen_mode = 'network' 59 | } 60 | 61 | return moduleSettings; 62 | } 63 | 64 | function drawPreviewNetwork() { 65 | 66 | var moduleSettings = loadSettings(); 67 | 68 | if (moduleSettings.enable_director === false) { 69 | $('#director-settings').hide(); 70 | } else { 71 | $('#director-settings').show(); 72 | } 73 | 74 | if (moduleSettings.scaling === true) { 75 | scalingSize = 15; 76 | } else { 77 | scalingSize = 0; 78 | } 79 | 80 | if (moduleSettings.alias_only) { 81 | var nodes = new vis.DataSet([{ 82 | id: 1, 83 | size: 25 + (scalingSize * 1.5), 84 | color: { 85 | border: 'green', 86 | background: 'white' 87 | }, 88 | group: 'up', 89 | label: 'host 1' 90 | }, 91 | { 92 | id: 2, 93 | size: 25 + scalingSize, 94 | color: { 95 | border: 'green', 96 | background: 'white' 97 | }, 98 | group: 'up', 99 | label: 'host 2' 100 | }, 101 | { 102 | id: 3, 103 | size: 25, 104 | color: { 105 | border: 'red', 106 | background: 'white' 107 | }, 108 | group: 'down', 109 | label: 'host 3' 110 | }, 111 | { 112 | id: 4, 113 | size: 25, 114 | color: { 115 | border: 'purple', 116 | background: 'white' 117 | }, 118 | group: 'unreachable', 119 | label: 'host 4' 120 | }, 121 | { 122 | id: 5, 123 | size: 25, 124 | color: { 125 | border: 'green', 126 | background: 'white' 127 | }, 128 | group: 'up', 129 | label: 'host 5' 130 | }, 131 | { 132 | id: 6, 133 | size: 25, 134 | color: { 135 | border: 'green', 136 | background: 'white' 137 | }, 138 | group: 'up', 139 | label: 'host 6' 140 | }, 141 | { 142 | id: 7, 143 | size: 25, 144 | color: { 145 | border: 'red', 146 | background: 'white' 147 | }, 148 | group: 'down', 149 | label: 'host 7' 150 | }, 151 | { 152 | id: 8, 153 | size: 25, 154 | color: { 155 | border: 'green', 156 | background: 'white' 157 | }, 158 | group: 'up', 159 | label: 'host 8' 160 | }, 161 | { 162 | id: 9, 163 | size: 25, 164 | color: { 165 | border: 'red', 166 | background: 'white' 167 | }, 168 | group: 'down', 169 | label: 'host 9' 170 | }, 171 | { 172 | id: 10, 173 | size: 25, 174 | color: { 175 | border: 'green', 176 | background: 'white' 177 | }, 178 | group: 'up', 179 | label: 'host 10' 180 | }, 181 | { 182 | id: 11, 183 | size: 25, 184 | color: { 185 | border: 'green', 186 | background: 'white' 187 | }, 188 | group: 'up', 189 | label: 'host 11' 190 | }, 191 | { 192 | id: 12, 193 | size: 25, 194 | color: { 195 | border: 'green', 196 | background: 'white' 197 | }, 198 | group: 'up', 199 | label: 'host 12' 200 | } 201 | 202 | 203 | ]); 204 | 205 | } else { 206 | var nodes = new vis.DataSet([{ 207 | id: 1, 208 | size: 25 + (scalingSize * 1.5), 209 | color: { 210 | border: 'green', 211 | background: 'white' 212 | }, 213 | group: 'up', 214 | label: 'host 1' + '\n(host-1)' 215 | }, 216 | { 217 | id: 2, 218 | size: 25 + scalingSize, 219 | color: { 220 | border: 'green', 221 | background: 'white' 222 | }, 223 | group: 'up', 224 | label: 'host 2' + '\n(host-2)' 225 | }, 226 | { 227 | id: 3, 228 | size: 25, 229 | color: { 230 | border: 'red', 231 | background: 'white' 232 | }, 233 | group: 'down', 234 | label: 'host 3' + '\n(host-3)' 235 | }, 236 | { 237 | id: 4, 238 | size: 25, 239 | color: { 240 | border: 'purple', 241 | background: 'white' 242 | }, 243 | group: 'unreachable', 244 | label: 'host 4' + '\n(host-4)' 245 | }, 246 | { 247 | id: 5, 248 | size: 25, 249 | color: { 250 | border: 'green', 251 | background: 'white' 252 | }, 253 | group: 'up', 254 | label: 'host 5' + '\n(host-5)' 255 | }, 256 | { 257 | id: 6, 258 | size: 25, 259 | color: { 260 | border: 'green', 261 | background: 'white' 262 | }, 263 | group: 'up', 264 | label: 'host 6' + '\n(host-6)' 265 | }, 266 | { 267 | id: 7, 268 | size: 25, 269 | color: { 270 | border: 'red', 271 | background: 'white' 272 | }, 273 | group: 'down', 274 | label: 'host 7' + '\n(host-7)' 275 | }, 276 | { 277 | id: 8, 278 | size: 25, 279 | color: { 280 | border: 'green', 281 | background: 'white' 282 | }, 283 | group: 'up', 284 | label: 'host 8' + '\n(host-8)' 285 | }, 286 | { 287 | id: 9, 288 | size: 25, 289 | color: { 290 | border: 'red', 291 | background: 'white' 292 | }, 293 | group: 'down', 294 | label: 'host 9' + '\n(host-9)' 295 | }, 296 | { 297 | id: 10, 298 | size: 25, 299 | color: { 300 | border: 'green', 301 | background: 'white' 302 | }, 303 | group: 'up', 304 | label: 'host 10' + '\n(host-10)' 305 | }, 306 | { 307 | id: 11, 308 | size: 25, 309 | color: { 310 | border: 'green', 311 | background: 'white' 312 | }, 313 | group: 'up', 314 | label: 'host 11' + '\n(host-11)' 315 | }, 316 | { 317 | id: 12, 318 | size: 25, 319 | color: { 320 | border: 'green', 321 | background: 'white' 322 | }, 323 | group: 'up', 324 | label: 'host 12' + '\n(host-12)' 325 | } 326 | 327 | 328 | ]); 329 | } 330 | 331 | var edges = new vis.DataSet([{ 332 | from: 3, 333 | to: 1 334 | }, 335 | { 336 | from: 4, 337 | to: 1 338 | }, 339 | { 340 | from: 5, 341 | to: 1 342 | }, 343 | { 344 | from: 6, 345 | to: 1 346 | }, 347 | { 348 | from: 7, 349 | to: 1 350 | }, 351 | { 352 | from: 8, 353 | to: 1 354 | }, { 355 | from: 9, 356 | to: 2 357 | }, 358 | { 359 | from: 10, 360 | to: 2 361 | }, 362 | { 363 | from: 11, 364 | to: 2 365 | } 366 | ]); 367 | 368 | if (moduleSettings.display_only_dependencies) { 369 | nodes.remove(12); 370 | } 371 | 372 | if (moduleSettings.display_up) { 373 | var upSize = moduleSettings.text_size; 374 | } else { 375 | var upSize = 0; 376 | } 377 | 378 | if (moduleSettings.display_down) { 379 | var downSize = moduleSettings.text_size; 380 | } else { 381 | var downSize = 0; 382 | } 383 | 384 | if (moduleSettings.display_unreachable) { 385 | var unreachableSize = moduleSettings.text_size; 386 | } else { 387 | var unreachableSize = 0; 388 | } 389 | 390 | if (moduleSettings.label_large_nodes) { 391 | 392 | nodes.update({ 393 | id: 1, 394 | font: { 395 | size: moduleSettings.text_size 396 | }, 397 | group: '' 398 | }); 399 | 400 | nodes.update({ 401 | id: 2, 402 | font: { 403 | size: moduleSettings.text_size 404 | }, 405 | group: '' 406 | }); 407 | 408 | } 409 | 410 | 411 | var container = document.getElementById('network-preview'); 412 | var data = { 413 | nodes: nodes, 414 | edges: edges 415 | }; 416 | 417 | const hierarchyOptions = { 418 | 419 | groups: { 420 | up: { 421 | font: { 422 | size: upSize 423 | } 424 | }, 425 | down: { 426 | font: { 427 | size: downSize 428 | } 429 | }, 430 | unreachable: { 431 | font: { 432 | size: unreachableSize 433 | } 434 | }, 435 | }, 436 | 437 | layout: { 438 | 439 | hierarchical: { 440 | enabled: true, 441 | levelSeparation: 200, 442 | nodeSpacing: 150, 443 | treeSpacing: 200, 444 | blockShifting: true, 445 | edgeMinimization: true, 446 | parentCentralization: true, 447 | direction: 'DU', 448 | sortMethod: 'directed' 449 | }, 450 | 451 | }, 452 | edges: { 453 | arrows: { 454 | middle: { 455 | enabled: true, 456 | scaleFactor: 1, 457 | type: 'arrow' 458 | } 459 | }, 460 | }, 461 | nodes: { 462 | shape: 'square', 463 | fixed: true, 464 | scaling: { 465 | min: 1, 466 | max: 15, 467 | label: { 468 | enabled: true, 469 | min: 14, 470 | max: 30, 471 | maxVisible: 30, 472 | drawThreshold: 5 473 | }, 474 | 475 | }, 476 | } 477 | }; 478 | const networkOptions = { 479 | 480 | groups: { 481 | up: { 482 | font: { 483 | size: upSize 484 | } 485 | }, 486 | down: { 487 | font: { 488 | size: downSize 489 | } 490 | }, 491 | unreachable: { 492 | font: { 493 | size: unreachableSize 494 | } 495 | }, 496 | }, 497 | 498 | layout: { 499 | improvedLayout: false, 500 | randomSeed: 728804 501 | }, 502 | edges: { 503 | smooth: { 504 | "forceDirection": "none", 505 | } 506 | }, 507 | 508 | nodes: { 509 | scaling: { 510 | label: true 511 | }, 512 | fixed: false, 513 | shape: 'dot' 514 | } 515 | }; 516 | 517 | if (moduleSettings.is_hierarchical) { 518 | var network = new vis.Network(container, data, hierarchyOptions); 519 | } else { 520 | var network = new vis.Network(container, data, networkOptions); 521 | } 522 | 523 | } 524 | 525 | function saveSettings() { 526 | 527 | var moduleSettings = loadSettings(); 528 | 529 | payload = { //convert to strings for db on frontend due t o PHP's handling of booleans->strings 530 | 531 | 'display_up': { 532 | 'value': (String(moduleSettings['display_up'])), 533 | 'type': 'bool' 534 | }, 535 | 536 | 'display_down': { 537 | 'value': (String(moduleSettings['display_down'])), 538 | 'type': 'bool' 539 | }, 540 | 541 | 'display_unreachable': { 542 | 'value': (String(moduleSettings['display_unreachable'])), 543 | 'type': 'bool' 544 | }, 545 | 546 | 'display_only_dependencies': { 547 | 'value': (String(moduleSettings['display_only_dependencies'])), 548 | 'type': 'bool' 549 | }, 550 | 551 | 'scaling': { 552 | 'value': (String(moduleSettings['scaling'])), 553 | 'type': 'bool' 554 | }, 555 | 556 | 'alias_only': { 557 | 'value': (String(moduleSettings['alias_only'])), 558 | 'type': 'bool' 559 | }, 560 | 561 | 'label_large_nodes': { 562 | 'value': (String(moduleSettings['label_large_nodes'])), 563 | 'type': 'bool' 564 | }, 565 | 566 | 'text_size': { 567 | 'value': (String(moduleSettings['text_size'])), 568 | 'type': 'int' 569 | }, 570 | 571 | // 'enable_director': { 572 | // 'value': (String(moduleSettings['enable_director'])), 573 | // 'type': 'bool' 574 | 575 | // }, 576 | 577 | // 'default_dependency_template': { 578 | // 'value': moduleSettings['default_dependency_template'], 579 | // 'type': 'string' 580 | // }, 581 | 582 | 'fullscreen_mode': { 583 | 'value' : moduleSettings['fullscreen_mode'], 584 | 'type' : 'string' 585 | } 586 | 587 | }; 588 | 589 | success = () => { 590 | 591 | // checkForSettingsConflicts() 592 | $('#notifications').append().html('
  • Settings Saved Successfully
  • '); 593 | } 594 | 595 | error = (error) => { 596 | errorHandler(error) 597 | } 598 | 599 | var saveSettings = storeGraphSettings(payload).then(success, error) 600 | 601 | } 602 | 603 | function loadSaved() { 604 | 605 | var settings = {} 606 | 607 | success = (data) => { 608 | settings = data['data'] 609 | } 610 | 611 | error = (error) => { 612 | 613 | errorHandler(error); 614 | 615 | throw error; 616 | } 617 | 618 | var settingsPromise = getSettings().then(success, error); 619 | 620 | Promise.all([settingsPromise]).then(() => { 621 | 622 | 623 | $("#host-mode-checkbox").prop('checked', settings.display_only_dependencies); 624 | $("#node-text-up-checkbox").prop('checked', settings.display_up); 625 | $("#node-text-down-checkbox").prop('checked', settings.display_down); 626 | $("#node-text-unreachable-checkbox").prop('checked', settings.display_unreachable); 627 | $("#scaling-mode-checkbox").prop('checked', settings.scaling); 628 | $("#text-size-range").val((settings.text_size) * 2); 629 | $('#label-mode-checkbox').prop('checked', settings.label_large_nodes); 630 | $('#alias-label-checkbox').prop('checked', settings.alias_only); 631 | $('#director-checkbox').prop('checked', settings.enable_director); 632 | $('#fullscreen-grid-checkbox').prop('checked', settings.fullscreen_mode === 'grid') 633 | $('#fullscreen-network-checkbox').prop('checked', settings.fullscreen_mode === 'network') 634 | 635 | $('#fullscreen-grid-checkbox').click(()=>{ 636 | $('#fullscreen-network-checkbox').prop('checked', false) 637 | }); 638 | 639 | $('#fullscreen-network-checkbox').click(()=>{ 640 | $('#fullscreen-grid-checkbox').prop('checked', false) 641 | }); 642 | 643 | 644 | // populateDependencyTemplateDropdown(settings.default_dependency_template); 645 | 646 | drawPreviewNetwork(); 647 | 648 | }); 649 | 650 | } 651 | 652 | // function populateDependencyTemplateDropdown(defaultTemplate) { 653 | // templateNames = []; 654 | 655 | // success = (data) => { 656 | 657 | // var templateDropdown = $("#dependency-template-field"); 658 | 659 | // for (i = 0; i < data['objects'].length; i++) { 660 | 661 | // templateDropdown.append(""); 662 | 663 | // } 664 | 665 | // $("#dependency-template-field").val(defaultTemplate); 666 | 667 | // } 668 | 669 | // error = (error) => { 670 | 671 | // handleError(error); 672 | 673 | // } 674 | 675 | // var templatesPromise = getTemplates().then(success, error) 676 | 677 | // } 678 | 679 | // function checkForSettingsConflicts(){ 680 | 681 | // } -------------------------------------------------------------------------------- /application/controllers/ModuleController.php: -------------------------------------------------------------------------------- 1 | getTabs()->add('Network', array( 34 | 'active' => false, 35 | 'label' => $this->translate('Network Map'), 36 | 'url' => 'dependency_plugin/module/network' 37 | )); 38 | 39 | $this->getTabs()->add('Hierarchy', array( 40 | 'active' => false, 41 | 'label' => $this->translate('Hierarchy Map'), 42 | 'url' => 'dependency_plugin/module/hierarchy' 43 | )); 44 | 45 | $this->getTabs()->add('Grid', array( 46 | 'active' => true, 47 | 'label' => $this->translate('Grid Map'), 48 | 'url' => 'dependency_plugin/module/statusGrid' 49 | )); 50 | 51 | } 52 | 53 | public function hierarchyAction() { 54 | 55 | $this->getTabs()->add('Network', array( 56 | 'active' => false, 57 | 'label' => $this->translate('Network Map'), 58 | 'url' => 'dependency_plugin/module/network' 59 | )); 60 | 61 | $this->getTabs()->add('Hierarchy', array( 62 | 'active' => true, 63 | 'label' => $this->translate('Hierarchy Map'), 64 | 'url' => 'dependency_plugin/module/hierarchy' 65 | )); 66 | 67 | $this->getTabs()->add('Grid', array( 68 | 'active' => false, 69 | 'label' => $this->translate('Grid Map'), 70 | 'url' => 'dependency_plugin/module/statusGrid' 71 | )); 72 | 73 | } 74 | 75 | public function networkAction() { 76 | 77 | $this->getTabs()->add('Network', array( 78 | 'active' => true, 79 | 'label' => $this->translate('Network Map'), 80 | 'url' => 'dependency_plugin/module/network' 81 | )); 82 | 83 | $this->getTabs()->add('Hierarchy', array( 84 | 'active' => false, 85 | 'label' => $this->translate('Hierarchy Map'), 86 | 'url' => 'dependency_plugin/module/hierarchy' 87 | )); 88 | 89 | $this->getTabs()->add('Grid', array( 90 | 'active' => false, 91 | 'label' => $this->translate('Grid Map'), 92 | 'url' => 'dependency_plugin/module/statusGrid' 93 | )); 94 | 95 | } 96 | 97 | public function kickstartAction() { 98 | 99 | $this->getTabs()->add('Graph Settings', array( 100 | 'active' => false, 101 | 'label' => $this->translate('Graph Settings'), 102 | 'url' => 'dependency_plugin/module/settings' 103 | )); 104 | 105 | 106 | $this->getTabs()->add('Module Settings', array( 107 | 'active' => true, 108 | 'label' => $this->translate('Module Settings'), 109 | 'url' => 'dependency_plugin/module/kickstart' 110 | )); 111 | 112 | 113 | } 114 | 115 | public function welcomeAction() { 116 | 117 | $this->getTabs()->add('Welcome', array( 118 | 'active' => true, 119 | 'label' => $this->translate('Welcome'), 120 | 'url' => 'dependency_plugin/module/welcome' 121 | )); 122 | 123 | } 124 | 125 | public function settingsAction() { 126 | 127 | $this->getTabs()->add('Graph Settings', array( 128 | 'active' => true, 129 | 'label' => $this->translate('Graph Settings'), 130 | 'url' => 'dependency_plugin/module/settings' 131 | )); 132 | 133 | $this->getTabs()->add('Module Settings', array( 134 | 'active' => false, 135 | 'label' => $this->translate('Module Settings'), 136 | 'url' => 'dependency_plugin/module/kickstart' 137 | )); 138 | 139 | } 140 | 141 | public function getresourcesAction(){ 142 | 143 | $dbArr = []; 144 | 145 | try { 146 | 147 | $resourcesfile = fopen("/etc/icingaweb2/resources.ini", 'r'); //get icinga resources (databases) 148 | 149 | while($line = fgets($resourcesfile)) { 150 | 151 | if(strpos($line, '[') !== false){ 152 | 153 | // echo $line; 154 | 155 | $dbname = explode('[', $line); 156 | $dbname = explode(']', $dbname[1]); 157 | array_push($dbArr, $dbname[0]); 158 | 159 | } 160 | 161 | } 162 | 163 | $resources['databases'] = $dbArr; 164 | 165 | } catch (Exception $e){ 166 | 167 | header('HTTP/1.1 500 Internal Server Error'); 168 | header('Content-Type: application/json; charset=UTF-8'); 169 | die(json_encode(array('message' => $e->getMessage(), 'code' => '500'))); 170 | } 171 | 172 | echo json_encode($resources); 173 | fclose($resourcesfile); 174 | 175 | exit; 176 | 177 | } 178 | 179 | function getResource() { 180 | 181 | try{ 182 | 183 | $resourcesfile = fopen('/etc/icingaweb2/modules/dependency_plugin/config.ini', 'r'); //get icinga resources (databases) 184 | 185 | }catch(Exception $e){ 186 | 187 | if(!file_exists('/etc/icingaweb2/modules/dependency_plugin/')){//config not created, module not kickstarted 188 | 189 | header('HTTP/1.1 500 Internal Server Error'); 190 | header('Content-Type: application/json; charset=UTF-8'); 191 | 192 | die(json_encode(array('message' => "setup", 'code' => '500'))); 193 | 194 | } else{ 195 | 196 | header('HTTP/1.1 500 Internal Server Error'); 197 | header('Content-Type: application/json; charset=UTF-8'); 198 | die(json_encode(array('message' => $e->getMessage(), 'code' => '500'))); 199 | } 200 | 201 | 202 | } 203 | 204 | 205 | $dbname = []; 206 | 207 | while($line = fgets($resourcesfile)) { 208 | 209 | if(strpos($line, 'resource') !== false){ 210 | 211 | $dbname = explode('=', $line); 212 | $dbname = explode('"', $dbname[1]); 213 | 214 | } 215 | 216 | } 217 | 218 | fclose($resourcesfile); 219 | 220 | return $dbname[1]; 221 | 222 | } 223 | 224 | public function storesettingsAction(){ 225 | 226 | 227 | // this function uses a built-in icinga web function saveIni(); which automatically saves any passed data to 228 | // /etc/icingaweb2/modules/name-of-moudle/config.ini 229 | 230 | $json = $_POST["json"]; 231 | 232 | $data = json_decode($json, true); 233 | 234 | // var_dump($data); 235 | 236 | // die; 237 | 238 | if($data != null){ 239 | 240 | $resource = $data[0]['value']; 241 | $host = $data[1]['value']; 242 | $port = $data[2]['value']; 243 | $username = $data[3]['value']; 244 | $password = $data[4]['value']; 245 | 246 | 247 | try { 248 | $db = IcingaDbConnection::fromResourceName($resource)->getDbAdapter(); 249 | 250 | $db->exec("TRUNCATE TABLE plugin_settings;"); //delete to only store latest settings 251 | 252 | $res = $db->insert('plugin_settings', array( 253 | 'api_user' => $username, 254 | 'api_password' => $password, 255 | 'api_endpoint' => $port, 256 | 'api_host' => $host, 257 | )); 258 | 259 | $config = $this->config(); 260 | $config->setSection('db', array('resource' => $resource)); 261 | 262 | $config->saveIni(); 263 | } catch (Exception $e) { 264 | header('HTTP/1.1 500 Internal Server Error'); 265 | header('Content-Type: application/json; charset=UTF-8'); 266 | 267 | die(json_encode( 268 | array( 269 | 'message' => "Error Saving To Database, Make sure correct database is created and selected", 270 | 'code' => '500', 271 | 'action'=>'setup' 272 | ) 273 | )); 274 | 275 | } 276 | 277 | echo $res; 278 | } 279 | 280 | exit; 281 | 282 | } 283 | 284 | public function getdependencyAction() { 285 | 286 | try { 287 | 288 | $resource = $this->getResource(); 289 | 290 | $db = IcingaDbConnection::fromResourceName($resource)->getDbAdapter(); 291 | $query = 'SELECT * from plugin_settings'; 292 | $vals = $db->fetchAll($query); 293 | if(!$vals){ //if no values 294 | throw new Exception('Empty Table'); //settings table empty 295 | } 296 | } 297 | catch(Exception $e){ 298 | 299 | header('HTTP/1.1 500 Internal Server Error'); 300 | header('Content-Type: application/json; charset=UTF-8'); 301 | 302 | die(json_encode(array('message' => $e->getMessage(), 'code' => '500'))); 303 | } 304 | 305 | $request_url = 'https://' . $vals[0]->api_host . ':'. $vals[0]->api_endpoint . '/v1/objects/dependencies'; 306 | $username = $vals[0]->api_user; 307 | $password = $vals[0]->api_password; 308 | $headers = array( 309 | 'Accept: application/json', 310 | 'X-HTTP-Method-Override: GET' 311 | ); 312 | 313 | $ch = curl_init(); 314 | 315 | curl_setopt_array($ch, array( 316 | 317 | CURLOPT_URL => $request_url, 318 | CURLOPT_HTTPHEADER => $headers, 319 | CURLOPT_USERPWD => $username . ":" . $password, 320 | CURLOPT_RETURNTRANSFER => true, 321 | CURLOPT_SSL_VERIFYPEER => false, 322 | CURLOPT_SSL_VERIFYHOST => false, 323 | )); 324 | 325 | $response = curl_exec($ch); 326 | $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); 327 | 328 | if($code === 401){ //echo detailed errors. 329 | header('HTTP/1.1 401 Unauthorized'); 330 | header('Content-Type: application/json; charset=UTF-8'); 331 | die(json_encode(array('message' => 'Unauthorized, Please Check Entered Credentials', 'code' => $code))); 332 | }else if($code != 200){ 333 | header('HTTP/1.1 500 Internal Server Error'); 334 | header('Content-Type: application/json; charset=UTF-8'); 335 | die(json_encode(array('message' => curl_error($ch), 'code' => $code))); 336 | } 337 | 338 | echo $response; 339 | exit; 340 | 341 | } 342 | 343 | public function gethostsAction(){ 344 | 345 | try { 346 | 347 | $resource = $this->getResource(); 348 | 349 | $db = IcingaDbConnection::fromResourceName($resource)->getDbAdapter(); 350 | 351 | $query = 'SELECT * from plugin_settings'; 352 | $vals = $db->fetchAll($query); 353 | 354 | if(!$vals){ 355 | throw new Exception('Empty Table'); 356 | 357 | } 358 | } 359 | catch(Exception $e){ 360 | 361 | header('HTTP/1.1 500 Internal Server Error'); 362 | header('Content-Type: application/json; charset=UTF-8'); 363 | die(json_encode(array('message' => $e->getMessage(), 'code' => "500"))); 364 | } 365 | 366 | $request_url = 'https://' . $vals[0]->api_host . ':'. $vals[0]->api_endpoint . '/v1/objects/hosts'; 367 | $username = $vals[0]->api_user; 368 | $password = $vals[0]->api_password; 369 | $headers = array( 370 | 'Accept: application/json', 371 | 'X-HTTP-Method-Override: GET' 372 | ); 373 | 374 | $ch = curl_init(); 375 | 376 | curl_setopt_array($ch, array( 377 | 378 | CURLOPT_URL => $request_url, 379 | CURLOPT_HTTPHEADER => $headers, 380 | CURLOPT_USERPWD => $username . ":" . $password, 381 | CURLOPT_RETURNTRANSFER => true, 382 | CURLOPT_SSL_VERIFYPEER => false, 383 | CURLOPT_SSL_VERIFYHOST => false, 384 | )); 385 | 386 | $response = curl_exec($ch); 387 | 388 | $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); 389 | 390 | if($code === 401){ 391 | header('HTTP/1.1 401 Unauthorized'); 392 | header('Content-Type: application/json; charset=UTF-8'); 393 | die(json_encode(array('message' => 'Unauthorized, Please Check Entered Credentials', 'code' => $code))); 394 | }else if($code != 200){ 395 | header('HTTP/1.1 500 Internal Server Error'); 396 | header('Content-Type: application/json; charset=UTF-8'); 397 | die(json_encode(array('message' => curl_error($ch), 'code' => $code))); 398 | // echo json_encode($code ); 399 | die; 400 | } 401 | echo $response; 402 | exit; 403 | 404 | } 405 | 406 | public function storenodepositionsAction(){ 407 | 408 | 409 | $resource = $this->getResource(); 410 | 411 | $db = IcingaDbConnection::fromResourceName($resource)->getDbAdapter(); 412 | 413 | $json = $_POST["json"]; 414 | 415 | $data = json_decode($json, true); 416 | 417 | if($data == 'RESET'){ 418 | $db->exec("TRUNCATE TABLE node_positions;"); 419 | } 420 | 421 | else if($data != null){ 422 | 423 | $result = $db->exec("TRUNCATE TABLE node_positions;"); 424 | 425 | foreach($data as $item){ 426 | 427 | $name = $item['id']; 428 | $node_x = $item['x']; 429 | $node_y = $item['y']; 430 | 431 | echo(gettype($item['y'])); 432 | 433 | $res = $db->insert('node_positions', array( 434 | 'node_name'=> $name, 'node_x' => $node_x, 'node_y' => $node_y 435 | )); 436 | 437 | 438 | if(!$res){ 439 | echo "An error occured while attempting to store nodes.\n"; 440 | exit; 441 | } 442 | } 443 | 444 | 445 | } 446 | 447 | exit; 448 | } 449 | 450 | public function getnodesAction(){ 451 | 452 | try { 453 | 454 | $resource = $this->getResource(); 455 | 456 | $db = IcingaDbConnection::fromResourceName($resource)->getDbAdapter(); 457 | 458 | $query = 'SELECT * from node_positions'; 459 | $vals = $db->fetchAll($query); 460 | 461 | if(!$vals){ 462 | throw new Exception('Empty Table'); 463 | } 464 | 465 | } catch(Exception $e){ 466 | 467 | if($e-> getMessage() == 'Empty Table'){ 468 | 469 | $json = json_encode('EMPTY!'); 470 | 471 | echo $json; 472 | 473 | exit; 474 | 475 | } else { 476 | header('HTTP/1.1 500 Internal Server Error'); 477 | header('Content-Type: application/json; charset=UTF-8'); 478 | die(json_encode(array('message' => $e->getMessage(), 'code' => '500'))); 479 | } 480 | } 481 | 482 | $json = json_encode($vals); 483 | 484 | echo $json; 485 | 486 | exit; 487 | } 488 | 489 | public function storegraphsettingsAction(){ 490 | // For some reason in this function, icingas database manager 'IcingaDbConnection' will store and retrieve data 491 | // based on whether the database is postgres or mysql, for example booleans for true and false are retrieved as 492 | // '1' and '' (empty string), due to reading the data using a php method to convert to strings on retrieval at 493 | // some point, and integers are retrieved as strings for MySql, and actual ints for Postgres. 494 | 495 | //to get around this, every thing is cast as an integer to avoid going through php's toString function 496 | 497 | $json = $_POST["json"]; 498 | 499 | $resource = $this->getResource(); 500 | 501 | $data = json_decode($json, true); 502 | 503 | if($data != null){ 504 | 505 | 506 | $db = IcingaDbConnection::fromResourceName($resource)->getDbAdapter(); 507 | 508 | $db->exec("TRUNCATE TABLE graph_settings;"); 509 | 510 | 511 | foreach($data as $name => $setting){ 512 | 513 | $res = $db->insert('graph_settings', array( //due to Zend reading bools as strings, converts true->1 false->"" 514 | 'setting_name' => $name, 515 | 'setting_value' => $setting["value"], 516 | 'setting_type' => $setting["type"] 517 | )); 518 | } 519 | } 520 | 521 | if(!$res){ 522 | echo "An error occured while attempting to store settings.\n"; 523 | exit; 524 | } 525 | 526 | exit; 527 | } 528 | 529 | public function getgraphsettingsAction() { 530 | 531 | try { 532 | 533 | $expectedNumberOfSettings = 9; //Number of settings expected out of database, change if setting added/removed 534 | 535 | $resource = $this->getResource(); 536 | 537 | $db = IcingaDbConnection::fromResourceName($resource)->getDbAdapter(); 538 | 539 | $query = 'SELECT * from graph_settings'; 540 | 541 | $vals = $db->fetchAll($query); 542 | 543 | $vals = (array_values($vals)); 544 | 545 | if(!$vals || count($vals) != $expectedNumberOfSettings){ //catch empty or incomplete settings table, provide default 546 | 547 | $db->exec("TRUNCATE TABLE graph_settings;"); 548 | 549 | $db->insert('graph_settings', array('setting_name' => 'default_dependency_template', 'setting_value' => '', 'setting_type' => 'string')); 550 | $db->insert('graph_settings', array('setting_name' => 'display_up', 'setting_value' => 'true', 'setting_type' => 'bool')); 551 | $db->insert('graph_settings', array('setting_name' => 'display_down', 'setting_value' => 'true', 'setting_type' => 'bool')); 552 | $db->insert('graph_settings', array('setting_name' => 'display_unreachable', 'setting_value' => 'true', 'setting_type' => 'bool')); 553 | $db->insert('graph_settings', array('setting_name' => 'display_only_dependencies', 'setting_value' => 'true', 'setting_type' => 'bool')); 554 | $db->insert('graph_settings', array('setting_name' => 'scaling', 'setting_value' => 'true', 'setting_type' => 'bool')); 555 | $db->insert('graph_settings', array('setting_name' => 'always_display_large_labels', 'setting_value' => 'true', 'setting_type' => 'bool')); 556 | $db->insert('graph_settings', array('setting_name' => 'alias_only', 'setting_value' => 'true', 'setting_type' => 'bool')); 557 | $db->insert('graph_settings', array('setting_name' => 'text_size', 'setting_value' => '25', 'setting_type' => 'int')); 558 | $db->insert('graph_settings', array('setting_name' => 'fullscreen_mode', 'setting_type'=> 'string', 'setting_value' => 'network')); 559 | 560 | $vals = $db->fetchAll($query); 561 | 562 | 563 | } 564 | } catch(Exception $e){ 565 | 566 | header('HTTP/1.1 500 Internal Server Error'); 567 | header('Content-Type: application/json; charset=UTF-8'); 568 | die(json_encode(array('message' => $e->getMessage(), 'code' => '500'))); 569 | 570 | exit; 571 | } 572 | 573 | $parsedSettings; 574 | 575 | //parse settings 576 | 577 | 578 | for ($i = 0; $i < count($vals); $i++) { 579 | 580 | if ($vals[$i]-> setting_type == 'bool') { 581 | $parsedSettings[$vals[$i]-> setting_name ] = ($vals[$i] -> setting_value === 'true'); 582 | } else if ($vals[$i] -> setting_type == 'int') { 583 | 584 | $parsedSettings[$vals[$i] -> setting_name] = ((int)($vals[$i] -> setting_value)); 585 | 586 | } else { 587 | 588 | $parsedSettings[$vals[$i] -> setting_name] = $vals[$i] -> setting_value; 589 | 590 | } 591 | } 592 | 593 | 594 | 595 | $json = json_encode($parsedSettings); 596 | 597 | echo $json; 598 | 599 | exit; 600 | } 601 | 602 | 603 | } 604 | 605 | ?> 606 | -------------------------------------------------------------------------------- /public/js/graphManager.js: -------------------------------------------------------------------------------- 1 | function getData() { 2 | 3 | var graphData = {}; 4 | var errors = {}; 5 | var positionData; 6 | 7 | processData = (response) => { 8 | 9 | graphData[response['type']] = response['data'] 10 | 11 | // console.log(graphData) 12 | } 13 | 14 | processError = (error) => { 15 | // errors[error['type']] = error['data'] 16 | throw error; 17 | } 18 | 19 | var hostPromise = getHosts().then(processData, processError) 20 | var dependencyPromise = getDependencies().then(processData, processError) 21 | var positionPromise = getNodePositions().then(processData, processError) 22 | var settingsPromise = getSettings().then(processData, processError) 23 | 24 | 25 | Promise.all([hostPromise, dependencyPromise, positionPromise, settingsPromise]).then(() => { 26 | 27 | if (window.location.href.indexOf('Fullscreen') > -1) { 28 | var isFullscreen = true; 29 | $('.fabs').hide(); 30 | } else { 31 | var isFullscreen = false; 32 | } 33 | 34 | if (window.location.href.indexOf('hierarchy') > -1) { 35 | var isHierarchical = true; 36 | $('.fabs').hide(); 37 | } else { 38 | var isHierarchical = false; 39 | } 40 | 41 | 42 | 43 | // console.log(graphData.dependencies['results'].length) 44 | // console.log(graphData.dependencies['results'].length === true) 45 | // if (!graphData.dependencies['results'].length) { 46 | // // if (graphData.hosts['results'].length) { 47 | // // console.log(graphData.hosts['results'].length) 48 | // // throw { 49 | // // type: 'configuration', 50 | // // message: 'Settings are preventing any hosts from being displayed, please enable showing hosts with no dependencies' 51 | // // } 52 | // // } 53 | // } 54 | 55 | formatDependencies(graphData.hosts, graphData.dependencies, isHierarchical, graphData.positions, isFullscreen, graphData.settings) 56 | 57 | }).catch((errors) => { 58 | errorHandler(errors); 59 | }); 60 | 61 | } 62 | 63 | function formatDependencies(hostData, dependencyData, isHierarchical, positionData, isFullscreen, settings) { 64 | //Function takes host state data, dependency data, and position data and builds a vis.js usable object using 65 | //the HostArray and Host objects. Neccesary due to needing match hosts with passed dependencies. 66 | 67 | 68 | var Hosts = new HostArray(); 69 | 70 | for (i = 0; i < hostData.results.length; i++) { 71 | 72 | Hosts.addHost(hostData.results[i].attrs); 73 | 74 | } 75 | 76 | for (i = 0; i < dependencyData.results.length; i++) { 77 | 78 | Hosts.addDependency(dependencyData.results[i].attrs); 79 | 80 | } 81 | 82 | if (positionData) { 83 | 84 | for (i = 0; i < positionData.length; i++) { 85 | 86 | Hosts.addPosition(positionData[i]); 87 | 88 | } 89 | } 90 | 91 | drawNetwork(Hosts, isHierarchical, isFullscreen, settings); 92 | 93 | } 94 | 95 | function drawNetwork(Hosts, isHierarchical, isFullscreen, settings) { 96 | 97 | //function uses data provided by the 'Hosts' and 'settings' objects to draw a vis.js network 98 | //In accordance with passed settings and data. 99 | 100 | var color_border = 'yellow'; 101 | 102 | var newHost = false; //is true when a host is present with no positon data. 103 | 104 | color_background = 'white' 105 | 106 | var nodes = new vis.DataSet([]); 107 | 108 | var edges = new vis.DataSet([]); 109 | 110 | for (i = 0; i < Hosts.length; i++) { 111 | 112 | currHost = Object.keys(Hosts.hostObject)[i]; //gets name of current host based on key iter 113 | 114 | if (settings.display_only_dependencies && !Hosts.hostObject[currHost].hasDependencies) { //skip adding node 115 | 116 | continue; 117 | 118 | } 119 | 120 | //colors based on host state 121 | 122 | if (Hosts.hostObject[currHost].status === 'DOWN') { 123 | color_border = 'red'; 124 | 125 | if (settings.display_down) { 126 | text_size = settings.text_size / 2; //parse int because an int is returned for MySql, a string for Postgres. 127 | } else { 128 | text_size = 0; 129 | } 130 | } 131 | 132 | if (Hosts.hostObject[currHost].status === 'UNREACHABLE') { 133 | color_border = 'purple'; 134 | 135 | if (settings.display_unreachable) { 136 | text_size = settings.text_size / 2; 137 | } else { 138 | text_size = 0; 139 | } 140 | } 141 | 142 | if (Hosts.hostObject[currHost].status === 'UP') { 143 | color_border = 'green'; 144 | 145 | if (settings.display_up) { 146 | text_size = settings.text_size / 2; 147 | } else { 148 | text_size = 0; 149 | } 150 | 151 | } 152 | 153 | 154 | if (settings.always_display_large_labels && Hosts.hostObject[currHost].isLargeNode > 3) { 155 | text_size = settings.text_size / 2; 156 | } 157 | 158 | if (settings.alias_only) { 159 | hostLabel = Hosts.hostObject[currHost].description; 160 | } else { 161 | hostLabel = (Hosts.hostObject[currHost].description + "\n(" + currHost + ")"); 162 | } 163 | 164 | if (Hosts.hostObject[currHost].hasPositionData) { 165 | 166 | nodes.update({ //vis.js function 167 | id: currHost, 168 | label: hostLabel, 169 | mass: (Hosts.hostObject[currHost].children.length / 4) + 1, 170 | color: { 171 | border: color_border, 172 | background: color_background 173 | }, 174 | 175 | font: { 176 | size: text_size, 177 | color: 'red' 178 | }, 179 | 180 | size: (Hosts.hostObject[currHost].children.length * 3 * settings.scaling + 20), 181 | 182 | x: Hosts.hostObject[currHost].position.x, //set x, y position 183 | y: Hosts.hostObject[currHost].position.y, 184 | }); 185 | 186 | } else { 187 | newHost = true; //has no position data, newly added 188 | 189 | nodes.update({ 190 | id: currHost, 191 | label: hostLabel, 192 | mass: (Hosts.hostObject[currHost].children.length / 4) + 1, 193 | color: { 194 | border: color_border, 195 | background: color_background 196 | }, 197 | 198 | font: { 199 | size: text_size, 200 | color: 'red' 201 | }, 202 | 203 | size: (Hosts.hostObject[currHost].children.length * 3 * settings.scaling + 20), 204 | 205 | }); 206 | } 207 | 208 | 209 | for (y = 0; y < Hosts.hostObject[currHost].parents.length; y++) { 210 | 211 | edges.update({ 212 | from: Hosts.hostObject[currHost].parents[y], 213 | to: currHost 214 | }); 215 | 216 | } 217 | 218 | } 219 | 220 | var networkData = { 221 | nodes: nodes, 222 | edges: edges 223 | } 224 | 225 | var container = document.getElementById('dependency-network'); 226 | 227 | const hierarchyOptions = { 228 | layout: { 229 | 230 | hierarchical: { 231 | enabled: true, 232 | levelSeparation: 200, 233 | nodeSpacing: 150, 234 | treeSpacing: 200, 235 | blockShifting: true, 236 | edgeMinimization: true, 237 | parentCentralization: true, 238 | direction: 'UD', 239 | sortMethod: 'directed' 240 | }, 241 | 242 | }, 243 | edges: { 244 | arrows: { 245 | middle: { 246 | enabled: true, 247 | scaleFactor: 1, 248 | type: 'arrow' 249 | } 250 | }, 251 | }, 252 | nodes: { 253 | shape: 'square', 254 | fixed: true, 255 | scaling: { 256 | min: 1, 257 | max: 15, 258 | label: { 259 | enabled: true, 260 | min: 14, 261 | max: 30, 262 | maxVisible: 30, 263 | drawThreshold: 5 264 | }, 265 | 266 | }, 267 | } 268 | }; 269 | 270 | const networkOptions = { 271 | 272 | layout: { 273 | improvedLayout: false, 274 | randomSeed: 728804 275 | }, 276 | edges: { 277 | smooth: { 278 | "forceDirection": "none", 279 | } 280 | }, 281 | 282 | nodes: { 283 | scaling: { 284 | label: true 285 | }, 286 | fixed: true, 287 | shape: 'dot' 288 | } 289 | }; 290 | 291 | if (isHierarchical) { //display using hierarchyOptions 292 | var network = new vis.Network(container, networkData, hierarchyOptions); 293 | 294 | } else if (isFullscreen) { //display using fullscreen (auto refresh) 295 | 296 | fullscreenMode(container, networkData); 297 | 298 | return; 299 | 300 | } else { 301 | 302 | var network = new vis.Network(container, networkData, networkOptions); 303 | 304 | if (Hosts.isNewNetwork) { 305 | simulateNewNetwork(network, nodes); //if there is no position data for the network, simulate network. 306 | 307 | } else if (newHost && !isHierarchical) { //if a new host was added, and it is not being displayed in hierarchical layout 308 | 309 | simulateChangedNetwork(network, nodes); 310 | 311 | } 312 | 313 | startEventListeners(network, networkData, settings); 314 | 315 | } 316 | } 317 | 318 | function simulateNewNetwork(network, nodes) { 319 | 320 | //simulates new network with a full number of physics iterations, neccecsary to layout an entire new network 321 | //somewhat accurately. Automatically saves position upon finishing simulation. 322 | 323 | network.setOptions({ 324 | nodes: { 325 | fixed: false //unlock nodes for physics sim 326 | } 327 | 328 | }); 329 | 330 | 331 | $('.fabs').hide(); 332 | 333 | $('#loadingBar').css('display', 'block'); 334 | 335 | $('#notifications').append().html('
  • Simulating New Network
  • '); 336 | 337 | network.on("stabilizationProgress", function (params) { //as network is simulating animate by percentage of physics iter complete 338 | var maxWidth = 496; 339 | var minWidth = 20; 340 | var widthFactor = params.iterations / params.total; 341 | var width = Math.max(minWidth, maxWidth * widthFactor); 342 | $('#bar').css("width", width); 343 | $('#text').html(Math.round(widthFactor * 100) + '%'); 344 | }); 345 | 346 | network.once("stabilizationIterationsDone", function () { 347 | $('#text').html('100%'); 348 | $('#bar').css("width", '496'); 349 | $('#loadingBar').css('opacity', '0'); 350 | // really clean the dom element 351 | setTimeout(function () { 352 | $('#loadingBar').css('display', 'none'); 353 | }, 500); 354 | $('.fabs').show(); 355 | 356 | network.storePositions(); //visjs function that adds X, Y coordinates of all nodes to the visjs node dataset that was used to draw the network. 357 | 358 | success = () => { 359 | 360 | network.setOptions({ 361 | nodes: { 362 | fixed: true 363 | } 364 | }); 365 | } 366 | 367 | error = (error) => { 368 | errorHandler(error); 369 | } 370 | 371 | var promise = storeNodePositions(nodes._data).then(success, error) 372 | 373 | }); 374 | } 375 | 376 | function simulateChangedNetwork(network, nodes) { 377 | //function simulates the network for a limited number of physics iterations, 378 | //usually enough to correctly place a newly added host/hosts. 379 | 380 | $('#notifications').append().html('
  • Network Change Detected
  • '); 381 | 382 | network.setOptions({ 383 | nodes: { 384 | fixed: false //unlock nodes 385 | } 386 | }); 387 | 388 | network.startSimulation(); //start new physics sim 389 | network.stabilize(800); //on sim for 200 iters, usually enough for the node to place itself automatically. 390 | network.once("stabilizationIterationsDone", function () { 391 | network.stopSimulation(); 392 | network.setOptions({ 393 | nodes: { 394 | fixed: true 395 | } 396 | }); 397 | network.storePositions(); //after new node added, resave network positions 398 | 399 | var promise = storeNodePositions(nodes._data).then(success, error); 400 | 401 | success = () => { 402 | 403 | $("#notification").html( 404 | "

    Network Change Detected

    " 405 | ).css({ 406 | "display": "block", 407 | }).delay(5000).fadeOut(); 408 | } 409 | 410 | error = (error) => { 411 | errorHandler(error) 412 | } 413 | 414 | }); 415 | } 416 | 417 | function startEventListeners(network, networkData, settings) { 418 | 419 | var font_size = 0; 420 | 421 | // function launches all event listeners for the network, and buttons. 422 | 423 | network.on("doubleClick", function (params) { //double click on node listener 424 | if (params.nodes[0] != undefined) { 425 | $('.fabs').hide(); 426 | 427 | let hostMonitoringAddress = ''; 428 | 429 | if (location.href.indexOf('/icingaweb2') > 1) { 430 | console.log("YOUR MOTHER WAS A HAMPSTER") 431 | hostMonitoringAddress = '/icingaweb2/monitoring/host/show?host=' 432 | } else { 433 | 434 | hostMonitoringAddress = '/monitoring/host/show?host='; 435 | } 436 | location.href = './network#!' + hostMonitoringAddress + params.nodes[0]; //redirect to host info page. 437 | 438 | } 439 | }); 440 | 441 | network.on("selectNode", function (params) { //on selecting node, background of label is made solid white for readabillity. 442 | var clickedNode = network.body.nodes[params.nodes[0]]; 443 | font_size = clickedNode.options.font.size; 444 | clickedNode.setOptions({ 445 | font: { 446 | size: 30, 447 | background: 'white', 448 | } 449 | }); 450 | }); 451 | 452 | network.on("deselectNode", function (params) { //on node deselect, label set back to transparent. 453 | 454 | var clickedNode = network.body.nodes[params.previousSelection.nodes[0]]; 455 | clickedNode.setOptions({ 456 | font: { 457 | size: font_size, 458 | background: 'none', 459 | } 460 | }); 461 | 462 | }); 463 | 464 | $('#edit-btn').click(function (params) { //on edit 465 | 466 | 467 | $('#notifications').append().html('
  • Editing Node Positions
  • '); 468 | 469 | network.setOptions({ //unlock nodes for editing 470 | nodes: { 471 | fixed: false 472 | }, 473 | }); 474 | 475 | $('.edit-fab').toggleClass('scale-out'); // show secondary FABs 476 | if ($('.edit-fab').hasClass('scale-out')) { //if already scaled out, second click hides secondary FABs and locks nodes 477 | network.setOptions({ 478 | nodes: { 479 | fixed: true 480 | } 481 | }); 482 | } 483 | }); 484 | 485 | 486 | $('#edit-btn-delete').click(function () { 487 | 488 | if (confirm("Reset All Network Positions?")) { 489 | 490 | 491 | 492 | success = () => { 493 | setTimeout(function () { 494 | 495 | window.location.replace("./network"); //on succes redirect to network. 496 | 497 | }, 2000); 498 | } 499 | 500 | error = (error) => { 501 | errorHandler(error); 502 | } 503 | 504 | var promise = storeNodePositions('RESET').then(success, error); 505 | } 506 | 507 | }); 508 | 509 | $('#edit-btn-save').click(function () { //on save 510 | 511 | network.setOptions({ 512 | nodes: { 513 | fixed: true 514 | } 515 | }); 516 | 517 | network.storePositions(); //visjs function that adds X, Y coordinates of all nodes to the visjs node dataset that was used to draw the network. 518 | 519 | success = () => { 520 | $('#notifications').append().html('
  • Nodes Positions Saved
  • '); 521 | 522 | } 523 | 524 | error = (error) => { 525 | errorHandler(error); 526 | } 527 | 528 | 529 | var promise = storeNodePositions(networkData.nodes._data).then(success, error); 530 | 531 | }); 532 | 533 | $('#edit-btn-fullscreen').click(() => { 534 | 535 | if (settings.fullscreen_mode === 'network') { 536 | window.location.replace("./network?showFullscreen"); 537 | } else { 538 | window.location.replace("./statusGrid?showFullscreen") 539 | } 540 | 541 | }); 542 | 543 | if (settings['enable_director'] === true) { 544 | 545 | $('#dependency-fabs').show(); 546 | 547 | $('#dependency-btn').click(() => { 548 | 549 | network.setOptions({ 550 | nodes: { 551 | fixed: false 552 | } 553 | }); 554 | 555 | $('.dependency-fab').toggleClass('scale-out'); // show secondary FABs 556 | 557 | 558 | if (!$('.edit-fab').hasClass('scale-out')) { //if already scaled out, second click hides secondary FABs and locks nodes 559 | $('.edit-fab').toggleClass('scale-out'); 560 | } 561 | 562 | $('#notifications').append().html('
  • Editing Dependencies
  • '); 563 | 564 | if (!settings.default_dependency_template) { 565 | alert('No Default Director Dependency Template Selected, Please Create or Select One.'); 566 | window.location.replace("./settings"); 567 | } 568 | 569 | $("#notification").html( 570 | "

    Editing Dependencies (Child -----> Parent)

    " 571 | ).css({ 572 | "display": "block", 573 | }) 574 | 575 | network.setOptions({ 576 | edges: { 577 | arrows: { 578 | from: true 579 | } 580 | } 581 | }); 582 | 583 | network.off('doubleClick'); 584 | 585 | network.off('selectNode'); 586 | 587 | network.off('deselectNode'); 588 | 589 | $('#dependency-btn').off(); 590 | 591 | $('#edit-btn').off(); 592 | 593 | startDependencyModeListeners(networkData, network, settings); 594 | 595 | }); 596 | } 597 | 598 | } 599 | 600 | function startDependencyModeListeners(networkData, network, settings) { 601 | 602 | var dependencies = []; 603 | 604 | var dependency = []; 605 | 606 | var updatedNodes = []; 607 | 608 | var container = document.getElementById('dependency-network'); 609 | 610 | 611 | network.on("selectNode", function (params) { //on selecting node, background of label is made solid white for readabillity. 612 | 613 | 614 | var selectedNode = network.body.nodes[params.nodes[0]]; 615 | 616 | dependency.push(selectedNode.id); 617 | 618 | if (dependency.length === 2) { 619 | 620 | drawnDependency = { 621 | "object_name": dependency[0] + " __to__ " + dependency[1], 622 | "object_type": "apply", 623 | "assign_filter": "host.name=%22" + dependency[0] + "%22", 624 | "imports": [settings.default_dependency_template], 625 | "apply_to": "host", 626 | "parent_host": dependency[1], 627 | } 628 | 629 | networkData.edges.update({ 630 | id: drawnDependency.object_name, 631 | from: dependency[1], 632 | to: dependency[0] 633 | }); 634 | 635 | dependencies.push(drawnDependency); 636 | 637 | dependency = []; 638 | 639 | } 640 | 641 | }); 642 | 643 | network.on("click", function (params) { 644 | if (params.nodes[0] === undefined) { 645 | dependency = []; 646 | } 647 | }); 648 | 649 | $('#dependency-btn').click(() => { 650 | 651 | $('.dependency-fab').toggleClass('scale-out'); // show secondary FABs 652 | if ($('.dependency-fab').hasClass('scale-out')) { //if already scaled out, second click hides secondary FABs and locks nodes 653 | network.setOptions({ 654 | nodes: { 655 | fixed: true 656 | }, 657 | edges: { 658 | arrows: { 659 | from: false 660 | } 661 | } 662 | }); 663 | } 664 | 665 | network.off('doubleClick'); 666 | 667 | network.off('selectNode'); 668 | 669 | network.off('deselectNode'); 670 | 671 | $('#dependency-btn').off(); 672 | 673 | startEventListeners(network, networkData, settings); 674 | 675 | 676 | }); 677 | 678 | $('#edit-btn').click(() => { 679 | 680 | $('#notifications').append().html('
  • Editing Node Positions
  • '); 681 | 682 | $('.dependency-fab').toggleClass('scale-out'); 683 | network.setOptions({ 684 | nodes: { 685 | fixed: false 686 | }, 687 | edges: { 688 | arrows: { 689 | from: false 690 | } 691 | } 692 | }); 693 | 694 | $('.edit-fab').toggleClass('scale-out') 695 | 696 | network.off('doubleClick'); 697 | 698 | network.off('selectNode'); 699 | 700 | network.off('deselectNode'); 701 | 702 | $('#dependency-btn').off(); 703 | $('#edit-btn').off(); 704 | 705 | startEventListeners(network, networkData, settings); 706 | 707 | }) 708 | 709 | 710 | $('#dependency-btn-undo').click(() => { 711 | 712 | if (dependencies.length === 0) { 713 | alert("Nothing to Undo"); 714 | return; 715 | } 716 | 717 | removedDependency = dependencies.pop(); 718 | 719 | 720 | networkData.edges.remove({ 721 | id: removedDependency.object_name 722 | }); 723 | 724 | }); 725 | 726 | 727 | 728 | $('#dependency-btn-save').click(function () { //on save 729 | 730 | 731 | importDependencies(dependencies); 732 | 733 | 734 | network.storePositions(); //visjs function that adds X, Y coordinates of all nodes to the visjs node dataset that was used to draw the network. 735 | 736 | $.ajax({ //ajax request to store into DB 737 | url: "./dependency_plugin/module/storeNodePositions", 738 | type: 'POST', 739 | data: { 740 | json: JSON.stringify(networkData.nodes._data) 741 | }, 742 | }); 743 | 744 | }); 745 | 746 | } 747 | 748 | function importDependencies(dependencies) { 749 | 750 | 751 | for (i = 0; i < dependencies.length; i++) { 752 | 753 | $.ajax({ 754 | url: "./director/dependency", 755 | type: 'POST', 756 | headers: { 757 | 'Accept': 'application/json' 758 | }, 759 | data: JSON.stringify(dependencies[i]), 760 | success: () => { 761 | deployChanges(); 762 | }, 763 | error: function (data) { 764 | console.log(data); 765 | alert('Adding dependency Unsuccessful:\n\n' + data.responseJSON['message']); 766 | return; 767 | } 768 | }); 769 | 770 | } 771 | 772 | 773 | 774 | 775 | } 776 | 777 | function deployChanges() { 778 | 779 | $.ajax({ 780 | url: "./director/config/deploy", 781 | type: 'POST', 782 | headers: { 783 | 'Accept': 'application/json' 784 | }, 785 | success: function (data) { 786 | 787 | $('#notifications').append().html('
  • Dependencies Saved Successfully
  • '); 788 | } 789 | 790 | }); 791 | 792 | } 793 | 794 | function Host(hostData) { 795 | 796 | //function accepts raw host data pulled from icinga 2 api, and formats it into a more usable format 797 | //while providing functions to add dependencies and position 798 | 799 | determineStatus = (state, wasReachable) => { 800 | 801 | if (state === 0) { 802 | return 'UP'; 803 | } else if (state === 1 && !wasReachable) { 804 | 805 | return 'UNREACHABLE' 806 | 807 | } else { 808 | 809 | return 'DOWN'; 810 | } 811 | 812 | } 813 | 814 | this.name = '' || hostData.name; 815 | this.status = determineStatus(hostData.state, hostData.last_reachable); 816 | this.description = '' || hostData.display_name; 817 | this.hasDependencies = false; 818 | this.parents = []; 819 | this.isLargeNode = false; 820 | this.group = '' || hostData.groups; 821 | this.children = []; 822 | this.position = { 823 | x: 0, 824 | y: 0 825 | }; 826 | this.hasPositionData = false; 827 | 828 | this.addParent = (parent) => { 829 | this.parents.push(parent); 830 | this.hasDependencies = true; 831 | } 832 | 833 | this.addChild = (Child) => { 834 | this.children.push(Child); 835 | this.hasDependencies = true; 836 | 837 | if (this.children.length > 3) { 838 | this.isLargeNode = true; 839 | } 840 | } 841 | 842 | this.setPositionData = (data) => { 843 | this.position.x = data.node_x; 844 | this.position.y = data.node_y; 845 | this.hasPositionData = true; 846 | } 847 | } 848 | 849 | function HostArray() { 850 | 851 | this.hostObject = {}; 852 | 853 | this.isNewNetwork = true; //if there any node has position data 854 | 855 | this.length = 0; 856 | 857 | this.addHost = (hostData) => { 858 | this.hostObject[hostData.name] = new Host(hostData) 859 | 860 | this.length++; 861 | } 862 | 863 | this.addDependency = (dependency) => { 864 | 865 | childName = dependency.child_host_name; 866 | 867 | parentName = dependency.parent_host_name; 868 | 869 | if (isInHosts(parentName)) { 870 | 871 | this.hostObject[parentName].addChild(childName); 872 | 873 | } 874 | 875 | if (isInHosts(childName)) { 876 | 877 | this.hostObject[childName].addParent(parentName); 878 | } 879 | 880 | } 881 | 882 | this.addPosition = (positionData) => { 883 | 884 | name = positionData.node_name; 885 | 886 | if (isInHosts(name)) { 887 | 888 | this.hostObject[name].setPositionData(positionData); 889 | 890 | this.isNewNetwork = false; 891 | 892 | } 893 | 894 | } 895 | 896 | isInHosts = (name) => { 897 | 898 | return (this.hostObject[name] != undefined); 899 | } 900 | 901 | 902 | } --------------------------------------------------------------------------------