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
22class CHostImporter extends CImporter {
23
24	/**
25	 * @var array  A list of host IDs which were created or updated to create an interface cache for those hosts.
26	 */
27	protected $processedHostIds = [];
28
29	/**
30	 * Import hosts.
31	 *
32	 * @param array $hosts
33	 *
34	 * @throws Exception
35	 */
36	public function import(array $hosts): void {
37		$hosts_to_create = [];
38		$hosts_to_update = [];
39		$valuemaps = [];
40		$template_linkage = [];
41		$templates_to_clear = [];
42
43		foreach ($hosts as $host) {
44			/*
45			 * Save linked templates for 2 purposes:
46			 *  - save linkages to add in case if 'create new' linkages is checked;
47			 *  - calculate missing linkages in case if 'delete missing' is checked.
48			 */
49			if (array_key_exists('templates', $host)) {
50				foreach ($host['templates'] as $template) {
51					$templateid = $this->referencer->findTemplateidByHost($template['name']);
52
53					if ($templateid === null) {
54						throw new Exception(_s('Template "%1$s" for host "%2$s" does not exist.', $template['name'], $host['host']));
55					}
56
57					$template_linkage[$host['host']][] = ['templateid' => $templateid];
58				}
59			}
60
61			unset($host['templates']);
62
63			$host = $this->resolveHostReferences($host);
64
65			if (array_key_exists('hostid', $host)
66					&& ($this->options['hosts']['updateExisting'] || $this->options['process_hosts'])) {
67				$hosts_to_update[] = $host;
68			}
69			elseif ($this->options['hosts']['createMissing']) {
70				if (array_key_exists('hostid', $host)) {
71					throw new Exception(_s('Host "%1$s" already exists.', $host['host']));
72				}
73
74				$hosts_to_create[] = $host;
75			}
76
77			if (array_key_exists('valuemaps', $host)) {
78				$valuemaps[$host['host']] = $host['valuemaps'];
79			}
80		}
81
82		if ($hosts_to_update) {
83			// Get template linkages to unlink and clear.
84			if ($this->options['templateLinkage']['deleteMissing']) {
85				// Get already linked templates.
86				$db_template_links = API::Host()->get([
87					'output' => ['hostids'],
88					'selectParentTemplates' => ['hostid'],
89					'hostids' => array_column($hosts_to_update, 'hostid'),
90					'preservekeys' => true
91				]);
92
93				foreach ($db_template_links as &$db_template_link) {
94					$db_template_link = array_column($db_template_link['parentTemplates'], 'templateid');
95				}
96				unset($db_template_link);
97
98				foreach ($hosts_to_update as $host) {
99					if (array_key_exists($host['host'], $template_linkage)) {
100						$templates_to_clear[$host['hostid']] = array_diff(
101							$db_template_links[$host['hostid']],
102							array_column($template_linkage[$host['host']], 'templateid')
103						);
104					}
105					else {
106						$templates_to_clear[$host['hostid']] = $db_template_links[$host['hostid']];
107					}
108				}
109			}
110
111			if ($this->options['hosts']['updateExisting']) {
112				$hosts_to_update = $this->addInterfaceIds($hosts_to_update);
113
114				API::Host()->update($hosts_to_update);
115			}
116
117			foreach ($hosts_to_update as $host) {
118				$this->processedHostIds[$host['host']] = $host['hostid'];
119
120				// Drop existing template linkages if 'delete missing' selected.
121				if (array_key_exists($host['hostid'], $templates_to_clear) && $templates_to_clear[$host['hostid']]) {
122					API::Host()->massRemove([
123						'hostids' => [$host['hostid']],
124						'templateids_clear' => $templates_to_clear[$host['hostid']]
125					]);
126				}
127
128				// Make new template linkages.
129				if ($this->options['templateLinkage']['createMissing']
130						&& array_key_exists($host['host'], $template_linkage)) {
131					API::Template()->massAdd([
132						'hosts' => $host,
133						'templates' => $template_linkage[$host['host']]
134					]);
135				}
136
137				$db_valuemaps = API::ValueMap()->get([
138					'output' => ['valuemapid', 'name'],
139					'hostids' => [$host['hostid']]
140				]);
141
142				if ($this->options['valueMaps']['createMissing'] && array_key_exists($host['host'], $valuemaps)) {
143					$valuemaps_to_create = [];
144					$valuemap_names = array_column($db_valuemaps, 'name');
145
146					foreach ($valuemaps[$host['host']] as $valuemap) {
147						if (!in_array($valuemap['name'], $valuemap_names)) {
148							$valuemap['hostid'] = $host['hostid'];
149							$valuemaps_to_create[] = $valuemap;
150						}
151					}
152
153					if ($valuemaps_to_create) {
154						API::ValueMap()->create($valuemaps_to_create);
155					}
156				}
157
158				if ($this->options['valueMaps']['updateExisting'] && array_key_exists($host['host'], $valuemaps)) {
159					$valuemaps_to_update = [];
160
161					foreach ($db_valuemaps as $db_valuemap) {
162						foreach ($valuemaps[$host['host']] as $valuemap) {
163							if ($db_valuemap['name'] === $valuemap['name']) {
164								$valuemap['valuemapid'] = $db_valuemap['valuemapid'];
165								$valuemaps_to_update[] = $valuemap;
166							}
167						}
168					}
169
170					if ($valuemaps_to_update) {
171						API::ValueMap()->update($valuemaps_to_update);
172					}
173				}
174
175				if ($this->options['valueMaps']['deleteMissing'] && $db_valuemaps) {
176					$valuemapids_to_delete = [];
177
178					if (array_key_exists($host['host'], $valuemaps)) {
179						$valuemap_names = array_column($valuemaps[$host['host']], 'name');
180
181						foreach ($db_valuemaps as $db_valuemap) {
182							if (!in_array($db_valuemap['name'], $valuemap_names)) {
183								$valuemapids_to_delete[] = $db_valuemap['valuemapid'];
184							}
185						}
186					}
187					else {
188						$valuemapids_to_delete = array_column($db_valuemaps, 'valuemapid');
189					}
190
191					if ($valuemapids_to_delete) {
192						API::ValueMap()->delete($valuemapids_to_delete);
193					}
194				}
195			}
196		}
197
198		if ($this->options['hosts']['createMissing'] && $hosts_to_create) {
199			$created_hosts = API::Host()->create($hosts_to_create);
200
201			foreach ($hosts_to_create as $index => $host) {
202				$hostid = $created_hosts['hostids'][$index];
203
204				$this->referencer->setDbHost($hostid, $host);
205				$this->processedHostIds[$host['host']] = $hostid;
206
207				if ($this->options['templateLinkage']['createMissing']
208					&& array_key_exists($host['host'], $template_linkage)) {
209					API::Template()->massAdd([
210						'hosts' => ['hostid' => $hostid],
211						'templates' => $template_linkage[$host['host']]
212					]);
213				}
214
215				if ($this->options['valueMaps']['createMissing'] && array_key_exists($host['host'], $valuemaps)) {
216					$valuemaps_to_create = [];
217
218					foreach ($valuemaps[$host['host']] as $valuemap) {
219						$valuemap['hostid'] = $hostid;
220						$valuemaps_to_create[] = $valuemap;
221					}
222
223					if ($valuemaps_to_create) {
224						API::ValueMap()->create($valuemaps_to_create);
225					}
226				}
227			}
228		}
229
230		// create interfaces cache interface_ref->interfaceid
231		$db_interfaces = API::HostInterface()->get([
232			'output' => API_OUTPUT_EXTEND,
233			'hostids' => $this->processedHostIds
234		]);
235
236		foreach ($hosts as $host) {
237			if (array_key_exists($host['host'], $this->processedHostIds)) {
238				foreach ($host['interfaces'] as $interface) {
239					$hostid = $this->processedHostIds[$host['host']];
240
241					if (!array_key_exists($hostid, $this->referencer->interfaces_cache)) {
242						$this->referencer->interfaces_cache[$hostid] = [];
243					}
244
245					foreach ($db_interfaces as $db_interface) {
246						if ($db_interface['hostid'] == $hostid
247								&& $db_interface['ip'] === $interface['ip']
248								&& $db_interface['dns'] === $interface['dns']
249								&& $db_interface['useip'] == $interface['useip']
250								&& $db_interface['port'] == $interface['port']
251								&& $db_interface['type'] == $interface['type']
252								&& $db_interface['main'] == $interface['main']) {
253
254							// Check SNMP additional fields.
255							if ($db_interface['type'] == INTERFACE_TYPE_SNMP) {
256								// Get fields that we can compare.
257								$array_diff = array_intersect_key($db_interface['details'], $interface['details']);
258
259								foreach (array_keys($array_diff) as $key) {
260									// Check field equality.
261									if ($db_interface['details'][$key] != $interface['details'][$key]) {
262										continue 2;
263									}
264								}
265							}
266
267							$this->referencer->interfaces_cache[$hostid][$interface['interface_ref']]
268								= $db_interface['interfaceid'];
269						}
270					}
271				}
272			}
273		}
274	}
275
276	/**
277	 * Get a list of created or updated host IDs.
278	 *
279	 * @return array
280	 */
281	public function getProcessedHostIds(): array {
282		return $this->processedHostIds;
283	}
284
285	/**
286	 * Change all references in host to database ids.
287	 *
288	 * @param array $host
289	 *
290	 * @return array
291	 *
292	 * @throws Exception
293	 */
294	protected function resolveHostReferences(array $host): array {
295		foreach ($host['groups'] as $index => $group) {
296			$groupid = $this->referencer->findGroupidByName($group['name']);
297
298			if ($groupid === null) {
299				throw new Exception(_s('Group "%1$s" for host "%2$s" does not exist.', $group['name'], $host['host']));
300			}
301
302			$host['groups'][$index] = ['groupid' => $groupid];
303		}
304
305		if (array_key_exists('proxy', $host)) {
306			if (!$host['proxy']) {
307				$proxyid = 0;
308			}
309			else {
310				$proxyid = $this->referencer->findProxyidByHost($host['proxy']['name']);
311
312				if ($proxyid === null) {
313					throw new Exception(_s('Proxy "%1$s" for host "%2$s" does not exist.', $host['proxy']['name'], $host['host']));
314				}
315			}
316
317			$host['proxy_hostid'] = $proxyid;
318		}
319
320		$hostid = $this->referencer->findHostidByHost($host['host']);
321
322		if ($hostid !== null) {
323			$host['hostid'] = $hostid;
324
325			if (array_key_exists('macros', $host)) {
326				foreach ($host['macros'] as &$macro) {
327					$hostmacroid = $this->referencer->findHostMacroid($hostid, $macro['macro']);
328
329					if ($hostmacroid !== null) {
330						$macro['hostmacroid'] = $hostmacroid;
331					}
332				}
333				unset($macro);
334			}
335		}
336
337		return $host;
338	}
339
340	/**
341	 * For existing hosts we need to set an interfaceid for existing interfaces or they will be added.
342	 *
343	 * @param array $hosts  Hosts from XML for which interfaces will be added.
344	 *
345	 * @return array
346	 */
347	protected function addInterfaceIds(array $hosts): array {
348		$db_interfaces = API::HostInterface()->get([
349			'output' => API_OUTPUT_EXTEND,
350			'hostids' => array_column($hosts, 'hostid'),
351			'preservekeys' => true
352		]);
353
354		// build lookup maps for:
355		// - interfaces per host
356		// - default (primary) interface ids per host per interface type
357		$db_host_interfaces = [];
358		$db_host_main_interfaceids = [];
359
360		foreach ($db_interfaces as $db_interface) {
361			$hostid = $db_interface['hostid'];
362
363			$db_host_interfaces[$hostid][] = $db_interface;
364			if ($db_interface['main'] == INTERFACE_PRIMARY) {
365				$db_host_main_interfaceids[$hostid][$db_interface['type']] = $db_interface['interfaceid'];
366			}
367		}
368
369		foreach ($hosts as &$host) {
370			// If interfaces in XML are non-existent or empty, delete the interfaces on host.
371
372			$hostid = $host['hostid'];
373
374			$main_interfaceids = array_key_exists($hostid, $db_host_main_interfaceids)
375				? $db_host_main_interfaceids[$hostid]
376				: [];
377
378			$reused_interfaceids = [];
379
380			foreach ($host['interfaces'] as &$interface) {
381				// check if an existing interfaceid from current host can be reused
382				// in case there is default (primary) interface in current host with same type
383				if ($interface['main'] == INTERFACE_PRIMARY
384						&& array_key_exists($interface['type'], $main_interfaceids)) {
385					$db_interfaceid = $main_interfaceids[$interface['type']];
386
387					$interface['interfaceid'] = $db_interfaceid;
388					$reused_interfaceids[$db_interfaceid] = true;
389				}
390			}
391			unset($interface);
392
393			// loop through all interfaces of current host and take interfaceids from ones that
394			// match completely, ignoring hosts from XML with set interfaceids and ignoring hosts
395			// from DB with reused interfaceids
396			foreach ($host['interfaces'] as &$interface) {
397				if (!array_key_exists($hostid, $db_host_interfaces)) {
398					continue;
399				}
400
401				foreach ($db_host_interfaces[$hostid] as $db_host_interface) {
402					$db_interfaceid = $db_host_interface['interfaceid'];
403
404					if (!array_key_exists('interfaceid', $interface)
405							&& !array_key_exists($db_interfaceid, $reused_interfaceids)
406							&& $db_host_interface['ip'] == $interface['ip']
407							&& $db_host_interface['dns'] == $interface['dns']
408							&& $db_host_interface['useip'] == $interface['useip']
409							&& $db_host_interface['port'] == $interface['port']
410							&& $db_host_interface['type'] == $interface['type']) {
411						$interface['interfaceid'] = $db_interfaceid;
412						$reused_interfaceids[$db_interfaceid] = true;
413						break;
414					}
415				}
416			}
417			unset($interface);
418		}
419		unset($host);
420
421		return $hosts;
422	}
423}
424