1<?php 2/* 3** Zabbix 4** Copyright (C) 2001-2021 Zabbix SIA 5** 6** This program is free software; you can redistribute it and/or modify 7** it under the terms of the GNU General Public License as published by 8** the Free Software Foundation; either version 2 of the License, or 9** (at your option) any later version. 10** 11** This program is distributed in the hope that it will be useful, 12** but WITHOUT ANY WARRANTY; without even the implied warranty of 13** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14** GNU General Public License for more details. 15** 16** You should have received a copy of the GNU General Public License 17** along with this program; if not, write to the Free Software 18** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19**/ 20 21 22/** 23 * Class to perform low level application related actions. 24 */ 25class CApplicationManager { 26 27 /** 28 * Create new applications. 29 * 30 * @param array $applications 31 * 32 * @return array 33 */ 34 public function create(array $applications) { 35 $insertApplications = $applications; 36 foreach ($insertApplications as &$app) { 37 unset($app['applicationTemplates']); 38 } 39 unset($app); 40 41 $applicationids = DB::insertBatch('applications', $insertApplications); 42 43 $applicationTemplates = []; 44 foreach ($applications as $anum => &$application) { 45 $application['applicationid'] = $applicationids[$anum]; 46 47 if (isset($application['applicationTemplates'])) { 48 foreach ($application['applicationTemplates'] as $applicationTemplate) { 49 $applicationTemplates[] = [ 50 'applicationid' => $application['applicationid'], 51 'templateid' => $applicationTemplate['templateid'] 52 ]; 53 } 54 } 55 } 56 unset($application); 57 58 // link inherited apps 59 DB::insertBatch('application_template', $applicationTemplates); 60 61 return $applications; 62 } 63 64 /** 65 * Update applications. 66 * 67 * @param array $applications 68 * 69 * @return array 70 */ 71 public function update(array $applications) { 72 $update = []; 73 $applicationTemplates = []; 74 foreach ($applications as $application) { 75 if (isset($application['applicationTemplates'])) { 76 foreach ($application['applicationTemplates'] as $applicationTemplate) { 77 $applicationTemplates[] = $applicationTemplate; 78 } 79 unset($application['applicationTemplates']); 80 } 81 82 $update[] = [ 83 'values' => $application, 84 'where' => ['applicationid' => $application['applicationid']] 85 ]; 86 } 87 DB::update('applications', $update); 88 89 // replace existing application templates 90 if ($applicationTemplates) { 91 $dbApplicationTemplates = DBfetchArray(DBselect( 92 'SELECT * '. 93 ' FROM application_template at'. 94 ' WHERE '.dbConditionInt('at.applicationid', zbx_objectValues($applications, 'applicationid')) 95 )); 96 DB::replace('application_template', $dbApplicationTemplates, $applicationTemplates); 97 } 98 99 return $applications; 100 } 101 102 /** 103 * Link applications in template to hosts. 104 * 105 * @param $templateId 106 * @param $hostIds 107 * 108 * @return bool 109 */ 110 public function link($templateId, $hostIds) { 111 $hostIds = zbx_toArray($hostIds); 112 113 // fetch template applications 114 $applications = DBfetchArray(DBselect( 115 'SELECT a.applicationid,a.name,a.hostid'. 116 ' FROM applications a'. 117 ' WHERE a.hostid='.zbx_dbstr($templateId) 118 )); 119 120 $this->inherit($applications, $hostIds); 121 122 return true; 123 } 124 125 /** 126 * Inherit passed applications to hosts. 127 * If $hostIds is empty that means that we need to inherit all $applications to hosts which are linked to templates 128 * where $applications belong. 129 * 130 * Usual use case is: 131 * inherit is called with some $hostIds passed 132 * new applications are created/updated 133 * inherit is called again with created/updated applications but empty $hostIds 134 * if any of new applications belongs to template, inherit it to all hosts linked to that template 135 * 136 * @param array $applications 137 * @param array $hostIds 138 * 139 * @return bool 140 */ 141 public function inherit(array $applications, array $hostIds = []) { 142 $hostTemplateMap = $this->getChildHostsFromApplications($applications, $hostIds); 143 if (empty($hostTemplateMap)) { 144 return true; 145 } 146 147 $hostApps = $this->getApplicationMapsByHostIds(array_keys($hostTemplateMap)); 148 $preparedApps = $this->prepareInheritedApps($applications, $hostTemplateMap, $hostApps); 149 $inheritedApps = $this->save($preparedApps); 150 151 $applications = zbx_toHash($applications, 'applicationid'); 152 153 // update application linkage 154 $oldApplicationTemplateIds = []; 155 $movedAppTemplateIds = []; 156 $childAppIdsPairs = []; 157 $oldChildApps = []; 158 foreach ($inheritedApps as $newChildApp) { 159 $oldChildAppsByTemplateId = $hostApps[$newChildApp['hostid']]['byTemplateId']; 160 161 foreach ($newChildApp['applicationTemplates'] as $applicationTemplate) { 162 // check if the parent of this application had a different child on the same host 163 if (isset($oldChildAppsByTemplateId[$applicationTemplate['templateid']]) 164 && $oldChildAppsByTemplateId[$applicationTemplate['templateid']]['applicationid'] != $newChildApp['applicationid']) { 165 166 // if a different child existed, find the template-application link and remove it later 167 $oldChildApp = $oldChildAppsByTemplateId[$applicationTemplate['templateid']]; 168 $oldApplicationTemplates = zbx_toHash($oldChildApp['applicationTemplates'], 'templateid'); 169 $oldApplicationTemplateIds[] = $oldApplicationTemplates[$applicationTemplate['templateid']]['application_templateid']; 170 171 // save the IDs of the affected templates and old 172 if (isset($applications[$applicationTemplate['templateid']])) { 173 $movedAppTemplateIds[] = $applications[$applicationTemplate['templateid']]['hostid']; 174 $childAppIdsPairs[$oldChildApp['applicationid']] = $newChildApp['applicationid']; 175 } 176 177 $oldChildApps[] = $oldChildApp; 178 } 179 } 180 } 181 182 // move all items and web scenarios from the old app to the new 183 if ($childAppIdsPairs) { 184 $this->moveInheritedItems($movedAppTemplateIds, $childAppIdsPairs); 185 $this->moveInheritedHttpTests($movedAppTemplateIds, $childAppIdsPairs); 186 } 187 188 // delete old application links 189 if ($oldApplicationTemplateIds) { 190 DB::delete('application_template', [ 191 'application_templateid' => $oldApplicationTemplateIds 192 ]); 193 } 194 195 // delete old children that have only one parent 196 $delAppIds = []; 197 foreach ($oldChildApps as $app) { 198 if (count($app['applicationTemplates']) == 1) { 199 $delAppIds[] = $app['applicationid']; 200 } 201 } 202 if ($delAppIds && $emptyIds = $this->fetchEmptyIds($delAppIds)) { 203 $this->delete($emptyIds); 204 } 205 206 $this->inherit($inheritedApps); 207 208 return true; 209 } 210 211 /** 212 * Replaces applications for all items inherited from templates $templateIds according to the map given in 213 * $appIdPairs. 214 * 215 * @param array $templateIds 216 * @param array $appIdPairs an array of source application ID - target application ID pairs 217 */ 218 protected function moveInheritedItems(array $templateIds, array $appIdPairs) { 219 // fetch existing item application links for all items inherited from template $templateIds 220 $itemApps = DBfetchArray(DBselect( 221 'SELECT ia2.itemappid,ia2.applicationid,ia2.itemid'. 222 ' FROM items i,items i2,items_applications ia2'. 223 ' WHERE i.itemid=i2.templateid'. 224 ' AND i2.itemid=ia2.itemid'. 225 ' AND '.dbConditionInt('i.hostid', $templateIds). 226 ' AND '.dbConditionInt('ia2.applicationid', array_keys($appIdPairs)) 227 )); 228 229 // find item application links to target applications that may already exist 230 $query = DBselect( 231 'SELECT ia.itemid,ia.applicationid'. 232 ' FROM items_applications ia'. 233 ' WHERE '.dbConditionInt('ia.applicationid', $appIdPairs). 234 ' AND '.dbConditionInt('ia.itemid', zbx_objectValues($itemApps, 'itemid')) 235 ); 236 $exItemAppIds = []; 237 while ($row = DBfetch($query)) { 238 $exItemAppIds[$row['itemid']][$row['applicationid']] = $row['applicationid']; 239 } 240 241 $newAppItems = []; 242 $delAppItemIds = []; 243 foreach ($itemApps as $itemApp) { 244 // if no link to the target app exists, add a new one 245 if (!isset($exItemAppIds[$itemApp['itemid']][$appIdPairs[$itemApp['applicationid']]])) { 246 $newAppItems[$appIdPairs[$itemApp['applicationid']]][] = $itemApp['itemappid']; 247 } 248 // if the link to the target app already exists, delete the link to the old app 249 else { 250 $delAppItemIds[] = $itemApp['itemappid']; 251 } 252 } 253 254 // link the items to the new apps 255 foreach ($newAppItems as $targetAppId => $itemAppIds) { 256 DB::updateByPk('items_applications', $itemAppIds, [ 257 'applicationid' => $targetAppId 258 ]); 259 } 260 261 // delete old item application links 262 if ($delAppItemIds) { 263 DB::delete('items_applications', ['itemappid' => $delAppItemIds]); 264 } 265 } 266 267 /** 268 * Return IDs of applications that are not used by items or HTTP tests. 269 * 270 * @param array $applicationIds 271 * 272 * @return array 273 */ 274 public function fetchEmptyIds(array $applicationIds) { 275 return DBfetchColumn(DBselect( 276 'SELECT a.applicationid '. 277 ' FROM applications a'. 278 ' WHERE '.dbConditionInt('a.applicationid', $applicationIds). 279 ' AND NOT EXISTS (SELECT NULL FROM items_applications ia WHERE a.applicationid=ia.applicationid)'. 280 ' AND NOT EXISTS (SELECT NULL FROM httptest ht WHERE a.applicationid=ht.applicationid)' 281 ), 'applicationid'); 282 } 283 284 /** 285 * Return IDs of applications that are children only (!) of the given parents. 286 * 287 * @param array $parentApplicationIds 288 * 289 * @return array 290 */ 291 public function fetchExclusiveChildIds(array $parentApplicationIds) { 292 return DBfetchColumn(DBselect( 293 'SELECT at.applicationid '. 294 ' FROM application_template at'. 295 ' WHERE '.dbConditionInt('at.templateid', $parentApplicationIds). 296 ' AND NOT EXISTS (SELECT NULL FROM application_template at2 WHERE '. 297 ' at.applicationid=at2.applicationid'. 298 ' AND '.dbConditionInt('at2.templateid', $parentApplicationIds, true). 299 ')' 300 ), 'applicationid'); 301 } 302 303 /** 304 * Delete applications. 305 * 306 * @param array $applicationIds 307 */ 308 public function delete(array $applicationIds) { 309 // unset applications from http tests 310 DB::update('httptest', [ 311 'values' => ['applicationid' => null], 312 'where' => ['applicationid' => $applicationIds] 313 ]); 314 315 // remove Monitoring > Latest data toggle profile values related to given applications 316 DB::delete('profiles', ['idx' => 'web.latest.toggle', 'idx2' => $applicationIds]); 317 318 DB::delete('applications', ['applicationid' => $applicationIds]); 319 } 320 321 /** 322 * Replaces the applications for all http tests inherited from templates $templateIds according to the map given in 323 * $appIdPairs. 324 * 325 * @param array $templateIds 326 * @param array $appIdPairs an array of source application ID - target application ID pairs 327 */ 328 protected function moveInheritedHttpTests(array $templateIds, array $appIdPairs) { 329 // find all http tests inherited from the given templates and linked to the given applications 330 $query = DBselect( 331 'SELECT ht2.applicationid,ht2.httptestid'. 332 ' FROM httptest ht,httptest ht2'. 333 ' WHERE ht.httptestid=ht2.templateid'. 334 ' AND '.dbConditionInt('ht.hostid', $templateIds). 335 ' AND '.dbConditionInt('ht2.applicationid', array_keys($appIdPairs)) 336 ); 337 $targetAppHttpTestIds = []; 338 while ($row = DBfetch($query)) { 339 $targetAppHttpTestIds[$appIdPairs[$row['applicationid']]][] = $row['httptestid']; 340 } 341 342 // link the http test to the new apps 343 foreach ($targetAppHttpTestIds as $targetAppId => $httpTestIds) { 344 DB::updateByPk('httptest', $httpTestIds, [ 345 'applicationid' => $targetAppId 346 ]); 347 } 348 } 349 350 /** 351 * Get array with hosts that are linked with templates which passed applications belongs to as key and 352 * templateid that host is linked to as value. If second parameter $hostIds is not empty, result should contain 353 * only passed host IDs. 354 * 355 * Example: 356 * We have template T1 with application A1 and template T1 with application A2 both linked to hosts H1 and H2. 357 * When we pass A1 to this function it should return array like: 358 * array(H1_id => array(T1_id, T2_id), H2_id => array(T1_id, T2_id)); 359 * 360 * @param array $applications 361 * @param array $hostIds 362 * 363 * @return array 364 */ 365 protected function getChildHostsFromApplications(array $applications, array $hostIds = []) { 366 $hostsTemplatesMap = []; 367 368 $dbCursor = DBselect( 369 'SELECT ht.templateid,ht.hostid'. 370 ' FROM hosts_templates ht'. 371 ' WHERE '.dbConditionInt('ht.templateid', zbx_objectValues($applications, 'hostid')). 372 ($hostIds ? ' AND '.dbConditionInt('ht.hostid', $hostIds) : '') 373 ); 374 while ($dbHost = DBfetch($dbCursor)) { 375 $hostId = $dbHost['hostid']; 376 $templateId = $dbHost['templateid']; 377 378 if (!isset($hostsTemplatesMap[$hostId])) { 379 $hostsTemplatesMap[$hostId] = []; 380 } 381 $hostsTemplatesMap[$hostId][$templateId] = $templateId; 382 } 383 384 return $hostsTemplatesMap; 385 } 386 387 /** 388 * Generate application data for inheritance. Using passed parameters, decide if new application must be 389 * created on host or existing application must be updated. 390 * 391 * @param array $applications applications to prepare for inheritance 392 * @param array $hostsTemplatesMap map of host IDs to templates they are linked to 393 * @param array $hostApplications array of existing applications on the child host returned by 394 * self::getApplicationMapsByHostIds() 395 * 396 * @return array Return array with applications. Existing applications have "applicationid" key. 397 */ 398 protected function prepareInheritedApps(array $applications, array $hostsTemplatesMap, array $hostApplications) { 399 /* 400 * This variable holds array of working copies of results, indexed first by host ID (hence pre-filling 401 * with host IDs from $hostApplications as keys and empty arrays as values), and then by application name. 402 * For each host ID / application name pair, there is only one array with application data 403 * with key "applicationTemplates" which is updated, if application with same name is inherited from 404 * more than one template. In the end this variable gets looped through and plain result array is constructed. 405 */ 406 $newApplications = array_fill_keys(array_keys($hostApplications), []); 407 408 foreach ($applications as $application) { 409 $applicationId = $application['applicationid']; 410 411 foreach ($hostApplications as $hostId => $hostApplication) { 412 // If application template is not linked to host, skip it. 413 if (!isset($hostsTemplatesMap[$hostId][$application['hostid']])) { 414 continue; 415 } 416 417 if (!isset($newApplications[$hostId][$application['name']])) { 418 $newApplication = [ 419 'name' => $application['name'], 420 'hostid' => $hostId, 421 'applicationTemplates' => [] 422 ]; 423 } 424 else { 425 $newApplication = $newApplications[$hostId][$application['name']]; 426 } 427 428 $existingApplication = null; 429 430 /* 431 * Look for an application with the same name, if one exists - link the parent application to it. 432 * If no application with the same name exists, look for a child application via "templateid". 433 * Use it only if it has only one parent. Otherwise a new application must be created. 434 */ 435 if (isset($hostApplication['byName'][$application['name']])) { 436 $existingApplication = $hostApplication['byName'][$application['name']]; 437 } 438 elseif (isset($hostApplication['byTemplateId'][$applicationId]) 439 && count($hostApplication['byTemplateId'][$applicationId]['applicationTemplates']) == 1) { 440 $existingApplication = $hostApplication['byTemplateId'][$applicationId]; 441 } 442 443 if ($existingApplication) { 444 $newApplication['applicationid'] = $existingApplication['applicationid']; 445 446 // Add the new template link to an existing child application if it's not present yet. 447 $newApplication['applicationTemplates'] = isset($existingApplication['applicationTemplates']) 448 ? $existingApplication['applicationTemplates'] 449 : []; 450 451 $applicationTemplateIds = zbx_objectValues($newApplication['applicationTemplates'], 'templateid'); 452 453 if (!in_array($applicationId, $applicationTemplateIds)) { 454 $newApplication['applicationTemplates'][] = [ 455 'applicationid' => $newApplication['applicationid'], 456 'templateid' => $applicationId 457 ]; 458 } 459 } 460 else { 461 // If no matching child application exists, add a new one. 462 $newApplication['applicationTemplates'][] = ['templateid' => $applicationId]; 463 } 464 465 // Store new or updated application data so it can be reused. 466 $newApplications[$hostId][$application['name']] = $newApplication; 467 } 468 } 469 470 $result = []; 471 foreach ($newApplications as $hostId => $newApplicationsPerHost) { 472 foreach ($newApplicationsPerHost as $newApplication) { 473 $result[] = $newApplication; 474 } 475 } 476 477 return $result; 478 } 479 480 /** 481 * Get host applications for each passed host. 482 * Each host has two hashes with applications, one with name keys other with templateid keys. 483 * 484 * Resulting structure is: 485 * array( 486 * 'hostid1' => array( 487 * 'byName' => array(app1data, app2data, ...), 488 * 'nyTemplateId' => array(app1data, app2data, ...) 489 * ), ... 490 * ); 491 * 492 * @param array $hostIds 493 * 494 * @return array 495 */ 496 protected function getApplicationMapsByHostIds(array $hostIds) { 497 $hostApps = []; 498 foreach ($hostIds as $hostid) { 499 $hostApps[$hostid] = ['byName' => [], 'byTemplateId' => []]; 500 } 501 502 // fetch applications 503 $applications = DbFetchArrayAssoc(DBselect( 504 'SELECT a.applicationid,a.name,a.hostid'. 505 ' FROM applications a'. 506 ' WHERE '.dbConditionInt('a.hostid', $hostIds) 507 ), 'applicationid'); 508 $query = DBselect( 509 'SELECT *'. 510 ' FROM application_template at'. 511 ' WHERE '.dbConditionInt('at.applicationid', array_keys($applications)) 512 ); 513 while ($applicationTemplate = DbFetch($query)) { 514 $applications[$applicationTemplate['applicationid']]['applicationTemplates'][] = $applicationTemplate; 515 } 516 517 foreach ($applications as $app) { 518 $hostApps[$app['hostid']]['byName'][$app['name']] = $app; 519 520 if (isset($app['applicationTemplates'])) { 521 foreach ($app['applicationTemplates'] as $applicationTemplate) { 522 $hostApps[$app['hostid']]['byTemplateId'][$applicationTemplate['templateid']] = $app; 523 } 524 } 525 } 526 527 return $hostApps; 528 } 529 530 /** 531 * Save applications. If application has applicationid it gets updated otherwise a new one is created. 532 * 533 * @param array $applications 534 * 535 * @return array 536 */ 537 protected function save(array $applications) { 538 $appsCreate = []; 539 $appsUpdate = []; 540 541 foreach ($applications as $key => $app) { 542 if (isset($app['applicationid'])) { 543 $appsUpdate[] = $app; 544 } 545 else { 546 $appsCreate[$key] = $app; 547 } 548 } 549 550 if (!empty($appsCreate)) { 551 $newApps = $this->create($appsCreate); 552 foreach ($newApps as $key => $newApp) { 553 $applications[$key]['applicationid'] = $newApp['applicationid']; 554 } 555 } 556 if (!empty($appsUpdate)) { 557 $this->update($appsUpdate); 558 } 559 560 return $applications; 561 } 562} 563