1<?php
2
3# This file is a part of RackTables, a datacenter and server room management
4# framework. See accompanying file "COPYING" for the full copyright and
5# licensing information.
6
7/*
8*
9*  This file is a library of database access functions for RackTables.
10*
11*/
12
13$SQLSchema = array
14(
15	'object' => array
16	(
17		'table' => 'RackObject',
18		'columns' => array
19		(
20			'id' => 'id',
21			'name' => 'name',
22			'label' => 'label',
23			'asset_no' => 'asset_no',
24			'objtype_id' => 'objtype_id',
25			'rack_id' => '(SELECT MIN(rack_id) FROM RackSpace WHERE object_id = RackObject.id)',
26			'rack_id_2' => "(SELECT MIN(parent_entity_id) FROM EntityLink WHERE child_entity_type='object' AND child_entity_id = RackObject.id AND parent_entity_type = 'rack')",
27			'container_id' => "(SELECT MIN(parent_entity_id) FROM EntityLink WHERE child_entity_type='object' AND child_entity_id = RackObject.id AND parent_entity_type = 'object')",
28			'container_name' => '(SELECT name FROM RackObject WHERE id = container_id)',
29			'container_objtype_id' => '(SELECT objtype_id FROM RackObject WHERE id = container_id)',
30			'has_problems' => 'has_problems',
31			'comment' => 'comment',
32			'nports' => '(SELECT COUNT(*) FROM Port WHERE object_id = RackObject.id)',
33			'8021q_domain_id' => '(SELECT domain_id FROM VLANSwitch WHERE object_id = id LIMIT 1)',
34			'8021q_template_id' => '(SELECT template_id FROM VLANSwitch WHERE object_id = id LIMIT 1)',
35		),
36		'keycolumn' => 'id',
37		'ordcolumns' => array ('RackObject.name'),
38	),
39	'user' => array
40	(
41		'table' => 'UserAccount',
42		'columns' => array
43		(
44			'user_id' => 'user_id',
45			'user_name' => 'user_name',
46			'user_password_hash' => 'user_password_hash',
47			'user_realname' => 'user_realname',
48		),
49		'keycolumn' => 'user_id',
50		'ordcolumns' => array ('UserAccount.user_name'),
51	),
52	'ipv4net' => array
53	(
54		'table' => 'IPv4Network',
55		'columns' => array
56		(
57			'id' => 'id',
58			'ip_bin' => 'ip',
59			'mask' => 'mask',
60			'name' => 'name',
61			'comment' => 'comment',
62			'8021q' => '(SELECT GROUP_CONCAT(CONCAT(domain_id, "-", vlan_id) ORDER BY domain_id, vlan_id) FROM VLANIPv4 WHERE ipv4net_id = id)',
63		),
64		'keycolumn' => 'id',
65		'ordcolumns' => array ('IPv4Network.ip', 'IPv4Network.mask'),
66	),
67	'ipv6net' => array
68	(
69		'table' => 'IPv6Network',
70		'columns' => array
71		(
72			'id' => 'id',
73			'ip_bin' => 'ip',
74			'mask' => 'mask',
75			'name' => 'name',
76			'comment' => 'comment',
77			'8021q' => '(SELECT GROUP_CONCAT(CONCAT(domain_id, "-", vlan_id) ORDER BY domain_id, vlan_id) FROM VLANIPv6 WHERE ipv6net_id = id)',
78		),
79		'keycolumn' => 'id',
80		'ordcolumns' => array ('IPv6Network.ip', 'IPv6Network.mask'),
81	),
82	'file' => array
83	(
84		'table' => 'File',
85		'columns' => array
86		(
87			'id' => 'id',
88			'name' => 'name',
89			'type' => 'type',
90			'size' => 'size',
91			'ctime' => 'ctime',
92			'mtime' => 'mtime',
93			'atime' => 'atime',
94			'comment' => 'comment',
95		),
96		'keycolumn' => 'id',
97		'ordcolumns' => array ('File.name'),
98	),
99	'ipv4vs' => array
100	(
101		'table' => 'IPv4VS',
102		'columns' => array
103		(
104			'id' => 'id',
105			'vip_bin' => 'vip',
106			'vport' => 'vport',
107			'proto' => 'proto',
108			'name' => 'name',
109			'vsconfig' => 'vsconfig',
110			'rsconfig' => 'rsconfig',
111			'refcnt' => '(select count(vs_id) from IPv4LB where vs_id = id)',
112			//'vip' =>
113			//'dname' =>
114		),
115		'keycolumn' => 'id',
116		'ordcolumns' => array ('IPv4VS.vip', 'IPv4VS.proto', 'IPv4VS.vport'),
117	),
118	'ipvs' => array
119	(
120		'table' => 'VS',
121		'columns' => array
122		(
123			'id' => 'id',
124			'name' => 'name',
125			'vsconfig' => 'vsconfig',
126			'rsconfig' => 'rsconfig',
127		),
128		'keycolumn' => 'id',
129	),
130	'ipv4rspool' => array
131	(
132		'table' => 'IPv4RSPool',
133		'columns' => array
134		(
135			'id' => 'id',
136			'name' => 'name',
137			'refcnt' => '(select count(rspool_id) from IPv4LB where rspool_id = id)',
138			'rscount' => '(select count(rspool_id) from IPv4RS where rspool_id = IPv4RSPool.id)',
139			'vsconfig' => 'vsconfig',
140			'rsconfig' => 'rsconfig',
141		),
142		'keycolumn' => 'id',
143		'ordcolumns' => array ('IPv4RSPool.name', 'IPv4RSPool.id'),
144	),
145	'rack' => array
146	(
147		'table' => 'Rack',
148		'columns' => array
149		(
150			'id' => 'id',
151			'name' => 'name',
152			'height' => 'height',
153			'asset_no' => 'asset_no',
154			'has_problems' => 'has_problems',
155			'comment' => 'comment',
156			'row_id' => 'row_id',
157			'row_name' => 'row_name',
158			'location_id' => 'location_id',
159			'location_name' => 'location_name',
160		),
161		'keycolumn' => 'id',
162		'ordcolumns' => array ('location_name', 'row_name', 'sort_order', 'Rack.name'),
163		'pidcolumn' => 'row_id',
164	),
165	'row' => array
166	(
167		'table' => '`Row`',
168		'columns' => array
169		(
170			'id' => 'id',
171			'name' => 'name',
172			'location_id' => 'location_id',
173			'location_name' => 'location_name',
174			'rackc' => '(select count(Rack.id) from Rack where row_id = Row.id)',
175		),
176		'keycolumn' => 'id',
177		'ordcolumns' => array ('location_name', 'name'),
178	),
179	'location' => array
180	(
181		'table' => 'Location',
182		'columns' => array
183		(
184			'id' => 'id',
185			'name' => 'name',
186			'has_problems' => 'has_problems',
187			'comment' => 'comment',
188			'parent_id' => 'parent_id',
189			'parent_name' => 'parent_name',
190			'refcnt' => "(SELECT COUNT(child_entity_id) FROM EntityLink EL WHERE EL.parent_entity_type = 'location' AND EL.parent_entity_id = Location.id)",
191		),
192		'keycolumn' => 'id',
193		'ordcolumns' => array ('name'),
194	),
195	'vst' => array
196	(
197		'table' => 'VLANSwitchTemplate',
198		'columns' => array
199		(
200			'id' => 'id',
201			'description' => 'description',
202			'mutex_rev' => 'mutex_rev',
203			'saved_by' => 'saved_by',
204			'switchc' => '(SELECT COUNT(object_id) FROM VLANSwitch WHERE template_id = id)',
205			'rulec' => '(SELECT COUNT(rule_no) FROM VLANSTRule WHERE vst_id = id)',
206		),
207		'keycolumn' => 'id',
208		'ordcolumns' => array ('description'),
209	),
210);
211
212$searchfunc = array
213(
214	'object' => array
215	(
216		'by_port' => 'getPortSearchResults',
217		'by_attr' => 'getObjectAttrsSearchResults',
218		'by_iface' => 'getObjectIfacesSearchResults',
219		'by_nat' => 'getObjectNATSearchResults',
220		'by_cableid' => 'searchCableIDs',
221	),
222);
223
224$tablemap_8021q = array
225(
226	'desired' => array
227	(
228		'pvm' => 'PortVLANMode',
229		'pav' => 'PortAllowedVLAN',
230		'pnv' => 'PortNativeVLAN',
231	),
232	'cached' => array
233	(
234		'pvm' => 'CachedPVM',
235		'pav' => 'CachedPAV',
236		'pnv' => 'CachedPNV',
237	),
238);
239
240// VST roles
241$port_role_options = array
242(
243	'none' => 'none',
244	'access' => 'user: access only',
245	'trunk' => 'user: trunk only',
246	'anymode' => 'user: any mode',
247	'uplink' => 'system: uplink trunk',
248	'downlink' => 'system: downlink trunk',
249);
250
251//  flags to pass to scanIPSpace, scanIPv4Space, scanIPv6Space
252define ('IPSCAN_DO_ADDR', 1 << 0);
253define ('IPSCAN_DO_ALLOCS', 1 << 1);
254define ('IPSCAN_DO_VS', 1 << 2);
255define ('IPSCAN_DO_RS', 1 << 3);
256define ('IPSCAN_DO_NAT', 1 << 4);
257define ('IPSCAN_DO_LOG', 1 << 5);
258define ('IPSCAN_RTR_ONLY', 1 << 6);
259
260define ('IPSCAN_ANY', -1 ^ IPSCAN_RTR_ONLY);
261define ('IPSCAN_DO_SLB', IPSCAN_DO_VS | IPSCAN_DO_RS);
262
263$object_attribute_cache = array();
264
265// Return list of locations directly under a specified location
266function getLocations ($location_id)
267{
268	$result = usePreparedSelectBlade
269	(
270		'SELECT id, name FROM Location WHERE parent_id = ? ORDER BY name',
271		array ($location_id)
272	);
273	return reduceSubarraysToColumn (reindexById ($result->fetchAll (PDO::FETCH_ASSOC)), 'name');
274}
275
276// Return detailed information about one rack row.
277function getRowInfo ($row_id)
278{
279	$query =
280		"SELECT `Row`.id AS id, `Row`.name AS name, COUNT(Rack.id) AS count, " .
281		"IF(ISNULL(SUM(Rack.height)),0,SUM(Rack.height)) AS sum, " .
282		"Location.id AS location_id, Location.name AS location " .
283		"FROM `Row` LEFT JOIN Rack ON Rack.row_id = `Row`.id " .
284		"LEFT OUTER JOIN Location ON `Row`.location_id = Location.id " .
285		"WHERE `Row`.id = ? " .
286		"GROUP BY `Row`.id, Location.id";
287	$result = usePreparedSelectBlade ($query, array ($row_id));
288	if ($row = $result->fetch (PDO::FETCH_ASSOC))
289		return $row;
290	throw new EntityNotFoundException ('rackrow', $row_id);
291}
292
293// TODO: deprecated function. delete it
294function getAllRows ()
295{
296	return listCells ('row');
297}
298
299// Return list of rows directly under a specified location
300function getRows ($location_id)
301{
302	$result = usePreparedSelectBlade
303	(
304		'SELECT R.id, R.name FROM `Row` R ' .
305		'INNER JOIN EntityLink EL ON ' .
306		"EL.parent_entity_type = 'location' " .
307		"AND EL.child_entity_type = 'row' " .
308		"AND EL.child_entity_id = R.id " .
309		'WHERE EL.parent_entity_id = ? ' .
310		'ORDER BY R.name',
311		array ($location_id)
312	);
313	return reduceSubarraysToColumn (reindexById ($result->fetchAll (PDO::FETCH_ASSOC)), 'name');
314}
315
316function getRacks ($row_id)
317{
318	$result = usePreparedSelectBlade
319	(
320		'SELECT id, name, asset_no, height, sort_order, comment, row_name FROM Rack WHERE row_id = ? ORDER BY sort_order',
321		array ($row_id)
322	);
323	return reindexById ($result->fetchAll (PDO::FETCH_ASSOC));
324}
325
326# Return rack and row details for those objects on the list that have
327# at least one rackspace atom allocated to them.
328function getMountInfo ($object_ids)
329{
330	if (! count ($object_ids))
331		return array();
332	# In theory number of involved racks can be equal or even greater than the
333	# number of objects, but in practice it will often be tens times less. Hence
334	# the scope of the 1st pass is to tell IDs of all involved racks without
335	# fetching lots of duplicate data.
336	$result = usePreparedSelectBlade
337	(
338		'SELECT object_id, rack_id ' .
339		'FROM RackSpace ' .
340		'WHERE object_id IN(' . questionMarks (count ($object_ids)) . ') ' .
341		'GROUP BY object_id, rack_id ' .
342		'UNION ' .
343		'SELECT child_entity_id AS object_id, parent_entity_id AS rack_id ' .
344		'FROM EntityLink ' .
345		'WHERE child_entity_id IN(' . questionMarks (count ($object_ids)) . ') ' .
346		"AND parent_entity_type = 'rack' AND child_entity_type = 'object' " .
347		'ORDER BY rack_id ASC',
348		array_merge($object_ids, $object_ids)
349	);
350	$rackidlist = $objectlist = array();
351	foreach ($result as $row)
352	{
353		$objectlist[$row['object_id']][] = $row['rack_id'];
354		$rackidlist[] = $row['rack_id'];
355	}
356	unset ($result);
357	# short-cut to exit in case no object had rackspace allocated
358	if (! count ($rackidlist))
359	{
360		$ret = array();
361		foreach ($object_ids as $object_id)
362			$ret[$object_id] = array();
363		return $ret;
364	}
365	# Pass 2. Fetch shorter, but better extra data about the rows and racks,
366	# set displayed names for both.
367	$result = usePreparedSelectBlade
368	(
369		'SELECT Rack_.id as rack_id, Rack_.name AS rack_name, Rack_.label as rack_label, ' .
370		'parent_entity_id AS row_id, Row_.name AS row_name ' .
371		'FROM Object Rack_ ' .
372		"LEFT JOIN EntityLink ON (Rack_.id = child_entity_id AND parent_entity_type = 'row' AND child_entity_type = 'rack') " .
373		'LEFT JOIN Object Row_ ON parent_entity_id = Row_.id ' .
374		'WHERE Rack_.id IN(' . questionMarks (count ($rackidlist)) . ') ',
375		$rackidlist
376	);
377	$rackinfo = array();
378	foreach ($result as $row)
379	{
380		$rackinfo[$row['rack_id']] = array
381		(
382			'rack_id'   => $row['rack_id'],
383			'row_id'    => $row['row_id'],
384		);
385		if ('' != $row['rack_name'])
386			$rackinfo[$row['rack_id']]['rack_name'] = $row['rack_name'];
387		elseif ('' != $row['rack_label'])
388			$rackinfo[$row['rack_id']]['rack_name'] = $row['rack_label'];
389		else
390			$rackinfo[$row['rack_id']]['rack_name'] = 'rack#' . $row['rack_id'];
391		if ('' != $row['row_name'])
392			$rackinfo[$row['rack_id']]['row_name'] = $row['row_name'];
393		else
394			$rackinfo[$row['rack_id']]['row_name'] = 'row#' . $row['row_id'];
395	}
396	unset ($result);
397	# Pass 3. Combine retrieved data into returned array.
398	$ret = array();
399	foreach ($objectlist as $object_id => $racklist)
400		foreach ($racklist as $rack_id)
401			$ret[$object_id][] = $rackinfo[$rack_id];
402	return $ret;
403}
404
405# Return container details for a list of objects
406function getContainerInfo ($object_ids)
407{
408	if (! count ($object_ids))
409		return array ();
410	$result = usePreparedSelectBlade
411	(
412		'SELECT EL.child_entity_id, EL.parent_entity_id, RO.name, RO.objtype_id ' .
413		'FROM EntityLink EL ' .
414		'LEFT JOIN RackObject RO ON EL.parent_entity_id = RO.id ' .
415		'WHERE EL.child_entity_id IN (' . questionMarks (count ($object_ids)) . ') ' .
416		"AND EL.parent_entity_type = 'object' " .
417		"AND EL.child_entity_type = 'object' " .
418		'ORDER BY RO.name',
419		$object_ids
420	);
421	$ret = array ();
422	foreach ($result as $row)
423		$ret[$row['child_entity_id']][] = array
424		(
425			'container_id'    => $row['parent_entity_id'],
426			'container_dname' => formatObjectDisplayedName ($row['name'], $row['objtype_id'])
427		);
428	unset ($result);
429	return $ret;
430}
431
432// For a given realm return a list of entity records, each with
433// enough information for judgeCell() to execute.
434function listCells ($realm, $parent_id = 0)
435{
436	if (!$parent_id)
437	{
438		global $entityCache;
439		if (isset ($entityCache['complete'][$realm]))
440			return $entityCache['complete'][$realm];
441	}
442	global $SQLSchema;
443	if (!isset ($SQLSchema[$realm]))
444		throw new InvalidArgException ('realm', $realm);
445	$SQLinfo = $SQLSchema[$realm];
446	$qparams = array ();
447	$query = 'SELECT ';
448	foreach ($SQLinfo['columns'] as $alias => $expression)
449		// Automatically prepend table name to each single column, but leave all others intact.
450		$query .= ($alias == $expression ? "${SQLinfo['table']}.${alias}" : "${expression} as ${alias}") . ', ';
451	$query = trim($query, ', ');
452	$query .= " FROM ${SQLinfo['table']}";
453	if (isset ($SQLinfo['pidcolumn']) && $parent_id)
454	{
455		$query .= " WHERE ${SQLinfo['table']}.${SQLinfo['pidcolumn']} = ?";
456		$qparams[] = $parent_id;
457	}
458	if (isset ($SQLinfo['ordcolumns']))
459	{
460		$query .= " ORDER BY ";
461		foreach ($SQLinfo['ordcolumns'] as $oc)
462			$query .= "${oc}, ";
463		$query = trim($query, ', ');
464	}
465	$result = usePreparedSelectBlade ($query, $qparams);
466	$ret = array();
467	// Index returned result by the value of key column.
468	while ($row = $result->fetch (PDO::FETCH_ASSOC))
469	{
470		$entity_id = $row[$SQLinfo['keycolumn']];
471		$ret[$entity_id] = array ('realm' => $realm);
472		$ret[$entity_id]['etags'] = array();
473		foreach (array_keys ($SQLinfo['columns']) as $alias)
474			$ret[$entity_id][$alias] = $row[$alias];
475		// use the temporary rack_id_2 key and remove this key from the result array
476		if ($realm == 'object')
477		{
478			if (! isset ($ret[$entity_id]['rack_id']))
479				$ret[$entity_id]['rack_id'] = $ret[$entity_id]['rack_id_2'];
480			unset ($ret[$entity_id]['rack_id_2']);
481		}
482	}
483	unset($result);
484
485	// select tags and link them to previosly fetched entities
486	$query = 'SELECT entity_id, tag_id, user AS tag_user, UNIX_TIMESTAMP(date) AS tag_time FROM TagStorage WHERE entity_realm = ?';
487	$result = usePreparedSelectBlade ($query, array($realm));
488	global $taglist;
489	while ($row = $result->fetch (PDO::FETCH_ASSOC))
490	{
491		$tag_id = $row['tag_id'];
492		if (array_key_exists($row['entity_id'], $ret))
493			$ret[$row['entity_id']]['etags'][$tag_id] = array
494			(
495				'id' => $tag_id,
496				'tag' => $taglist[$tag_id]['tag'],
497				'parent_id' => $taglist[$tag_id]['parent_id'],
498				'color' => $taglist[$tag_id]['color'],
499				'description' => $taglist[$tag_id]['description'],
500				'user' => $row['tag_user'],
501				'time' => $row['tag_time'],
502			);
503	}
504	unset($result);
505	// Add necessary finish to the list before returning it. Maintain caches.
506	if (!$parent_id)
507		unset ($entityCache['partial'][$realm]);
508	if ($realm == 'object') // cache dict attributes of all objects to speed up autotags calculation
509		cacheDictAttrValues();
510	foreach ($ret as $entity_id => &$entity)
511	{
512		sortEntityTags ($entity); // changes ['etags'] and ['itags']
513		switch ($realm)
514		{
515		case 'object':
516			setDisplayedName ($entity); // set $entity['dname']
517			break;
518		case 'ipv4net':
519			$entity = array_merge ($entity, constructIPRange (ip4_int2bin ($entity['ip_bin']), $entity['mask']));
520			processIPNetVlans ($entity);
521			$entity['spare_ranges'] = array();
522			$entity['kidc'] = 0;
523			break;
524		case 'ipv6net':
525			$entity = array_merge ($entity, constructIPRange ($entity['ip_bin'], $entity['mask']));
526			processIPNetVlans ($entity);
527			$entity['spare_ranges'] = array();
528			$entity['kidc'] = 0;
529			break;
530		case 'ipv4vs':
531			$entity['vip'] = ip_format ($entity['vip_bin']);
532			setDisplayedName ($entity); // set $entity['dname']
533			$entity['vsconfig'] = dos2unix ($entity['vsconfig']);
534			$entity['rsconfig'] = dos2unix ($entity['rsconfig']);
535			break;
536		case 'ipv4rspool':
537			$entity['vsconfig'] = dos2unix ($entity['vsconfig']);
538			$entity['rsconfig'] = dos2unix ($entity['rsconfig']);
539			break;
540		case 'ipvs':
541			$entity['vsconfig'] = dos2unix ($entity['vsconfig']);
542			$entity['rsconfig'] = dos2unix ($entity['rsconfig']);
543			break;
544		default:
545			break;
546		}
547	}
548	if ($realm == 'ipv4net' || $realm == 'ipv6net')
549		fillIPNetsCorrelation ($ret);
550
551	foreach (array_keys ($ret) as $entity_id)
552	{
553		$entity = &$ret[$entity_id];
554		$entity['atags'] = callHook ('generateEntityAutoTags', $entity);
555		if (!$parent_id)
556			$entityCache['complete'][$realm][$entity_id] = $entity;
557		else
558			$entityCache['partial'][$realm][$entity_id] = $entity;
559	}
560
561	return $ret;
562}
563
564// Very much like listCells(), but return only one record requested
565// throws an exception if entity not exists
566function spotEntity ($realm, $id, $ignore_cache = FALSE)
567{
568	if (! $ignore_cache)
569	{
570		global $entityCache;
571		if (isset ($entityCache['complete'][$realm]))
572		{
573			if (isset ($entityCache['complete'][$realm][$id]))
574				return $entityCache['complete'][$realm][$id];
575		}
576		elseif (isset ($entityCache['partial'][$realm][$id]))
577			return $entityCache['partial'][$realm][$id];
578	}
579	global $SQLSchema;
580	if (!isset ($SQLSchema[$realm]))
581		throw new InvalidArgException ('realm', $realm);
582	$SQLinfo = $SQLSchema[$realm];
583	$query = 'SELECT tag_id, TagStorage.user as tag_user, UNIX_TIMESTAMP(TagStorage.date) AS tag_time';
584	foreach ($SQLinfo['columns'] as $alias => $expression)
585		// Automatically prepend table name to each single column, but leave all others intact.
586		$query .= ', ' . ($alias == $expression ? "${SQLinfo['table']}.${alias}" : "${expression} as ${alias}");
587	$query .= " FROM ${SQLinfo['table']} LEFT JOIN TagStorage on entity_realm = ? and entity_id = ${SQLinfo['table']}.${SQLinfo['keycolumn']}";
588	$query .= " WHERE ${SQLinfo['table']}.${SQLinfo['keycolumn']} = ?";
589	$result = usePreparedSelectBlade ($query, array ($realm, $id));
590	$ret = array();
591	global $taglist;
592	while ($row = $result->fetch (PDO::FETCH_ASSOC))
593		if (!isset ($ret['realm']))
594		{
595			$ret = array ('realm' => $realm);
596			foreach (array_keys ($SQLinfo['columns']) as $alias)
597				$ret[$alias] = $row[$alias];
598			// use the temporary rack_id_2 key and remove this key from the result array
599			if ($realm == 'object')
600			{
601				if (! isset ($ret['rack_id']))
602					$ret['rack_id'] = $ret['rack_id_2'];
603				unset ($ret['rack_id_2']);
604			}
605			$ret['etags'] = array();
606			if ($row['tag_id'] != NULL && isset ($taglist[$row['tag_id']]))
607				$ret['etags'][$row['tag_id']] = array
608				(
609					'id' => $row['tag_id'],
610					'tag' => $taglist[$row['tag_id']]['tag'],
611					'parent_id' => $taglist[$row['tag_id']]['parent_id'],
612					'color' => $taglist[$row['tag_id']]['color'],
613					'description' => $taglist[$row['tag_id']]['description'],
614					'user' => $row['tag_user'],
615					'time' => $row['tag_time'],
616				);
617		}
618		elseif (isset ($taglist[$row['tag_id']]))
619			$ret['etags'][$row['tag_id']] = array
620			(
621				'id' => $row['tag_id'],
622				'tag' => $taglist[$row['tag_id']]['tag'],
623				'parent_id' => $taglist[$row['tag_id']]['parent_id'],
624				'color' => $taglist[$row['tag_id']]['color'],
625				'description' => $taglist[$row['tag_id']]['description'],
626				'user' => $row['tag_user'],
627				'time' => $row['tag_time'],
628			);
629	unset ($result);
630	if (!isset ($ret['realm'])) // no rows were returned
631		throw new EntityNotFoundException ($realm, $id);
632	sortEntityTags ($ret); // changes ['etags'] and ['itags']
633	switch ($realm)
634	{
635	case 'object':
636		setDisplayedName ($ret); // set $ret['dname']
637		break;
638	case 'ipv4net':
639		processIPNetVlans ($ret);
640		$ret = array_merge ($ret, constructIPRange (ip4_int2bin ($ret['ip_bin']), $ret['mask']));
641		if (! fillNetKids ($ret))
642		{
643			$ret['spare_ranges'] = array();
644			$ret['kidc'] = 0;
645		}
646		break;
647	case 'ipv6net':
648		processIPNetVlans ($ret);
649		$ret = array_merge ($ret, constructIPRange ($ret['ip_bin'], $ret['mask']));
650		if (! fillNetKids ($ret))
651		{
652			$ret['spare_ranges'] = array();
653			$ret['kidc'] = 0;
654		}
655		break;
656	case 'ipv4vs':
657		$ret['vip'] = ip_format ($ret['vip_bin']);
658		setDisplayedName ($ret); // set $ret['dname']
659		$ret['vsconfig'] = dos2unix ($ret['vsconfig']);
660		$ret['rsconfig'] = dos2unix ($ret['rsconfig']);
661		break;
662	case 'ipv4rspool':
663		$ret['vsconfig'] = dos2unix ($ret['vsconfig']);
664		$ret['rsconfig'] = dos2unix ($ret['rsconfig']);
665		break;
666	case 'ipvs':
667		$ret['vsconfig'] = dos2unix ($ret['vsconfig']);
668		$ret['rsconfig'] = dos2unix ($ret['rsconfig']);
669		break;
670	default:
671		break;
672	}
673
674	$ret['atags'] = generateEntityAutoTags ($ret);
675	if (! $ignore_cache)
676		$entityCache['partial'][$realm][$id] = $ret;
677	return $ret;
678}
679
680function fillNetKids (&$net_cell)
681{
682	if ($net_cell['realm'] == 'ipv6net')
683	{
684		$table = 'IPv6Network';
685		$ip_first = $net_cell['ip_bin'];
686		$ip_last = ip_last ($net_cell);
687	}
688	else
689	{
690		$table = 'IPv4Network';
691		$ip_first = ip4_bin2db ($net_cell['ip_bin']);
692		$ip_last = ip4_bin2db (ip_last ($net_cell));
693	}
694
695	$result = usePreparedSelectBlade ("
696SELECT id, ip as ip_bin, mask FROM $table
697WHERE ip BETWEEN ? AND ? AND mask >= ?
698ORDER BY ip, mask
699", array ($ip_first, $ip_last, $net_cell['mask']));
700	$nets = array();
701	while ($net_row = $result->fetch (PDO::FETCH_ASSOC))
702	{
703		$ip_bin = $net_row['ip_bin'];
704		if ($net_cell['realm'] == 'ipv4net')
705			$ip_bin = ip4_int2bin ($ip_bin);
706		$nets[] = constructIPRange ($ip_bin, $net_row['mask']) +
707			array(
708				'id' => $net_row['id'],
709				'spare_ranges' => array(),
710				'kidc' => 0,
711			);
712	}
713	unset ($result);
714
715	fillIPNetsCorrelation ($nets, 1);
716	if (is_array ($nets[0]) && $nets[0]['id'] == $net_cell['id'])
717	{
718		$net_cell['spare_ranges'] = $nets[0]['spare_ranges'];
719		$net_cell['kidc'] = $nets[0]['kidc'];
720		return TRUE;
721	}
722	return FALSE;
723}
724
725// This function can be used with array_walk().
726function amplifyCell (&$record, $dummy = NULL)
727{
728	switch ($record['realm'])
729	{
730	case 'object':
731		$record['ports'] = getObjectPortsAndLinks ($record['id']);
732		$record['ipv4'] = getObjectIPv4Allocations ($record['id']);
733		$record['ipv6'] = getObjectIPv6Allocations ($record['id']);
734		$record['nat4'] = getNATv4ForObject ($record['id']);
735		$record['files'] = getFilesOfEntity ($record['realm'], $record['id']);
736		break;
737	case 'file':
738		$record['links'] = getFileLinks ($record['id']);
739		break;
740	case 'location':
741		$record['locations'] = getLocations ($record['id']);
742		$record['rows'] = getRows ($record['id']);
743		break;
744	case 'row':
745		$record['racks'] = getRacks ($record['id']);
746		break;
747	case 'rack':
748		// start with default rackspace
749		for ($i = $record['height']; $i > 0; $i--)
750			for ($locidx = 0; $locidx < 3; $locidx++)
751				$record[$i][$locidx]['state'] = 'F';
752		// load difference
753		$query =
754			"select unit_no, atom, state, object_id, has_problems " .
755			"from RackSpace LEFT JOIN Object ON Object.id = object_id where rack_id = ? and " .
756			"unit_no between 1 and ? order by unit_no";
757		$result = usePreparedSelectBlade ($query, array ($record['id'], $record['height']));
758		global $loclist;
759		$mounted_objects = array();
760		// fetch Zero-U mounted objects
761		foreach (getChildren ($record, 'object') as $child)
762			$mounted_objects[$child['id']] = TRUE;
763
764		$rows = $result->fetchAll (PDO::FETCH_ASSOC);
765		unset ($result);
766		foreach ($rows as $row)
767		{
768			$record[$row['unit_no']][$loclist[$row['atom']]]['state'] = $row['state'];
769			$record[$row['unit_no']][$loclist[$row['atom']]]['object_id'] = $row['object_id'];
770			$record[$row['unit_no']][$loclist[$row['atom']]]['hl'] = $row['has_problems'] == 'yes' ? 'w' : '';
771			if ($row['state'] == 'T' && $row['object_id'] != NULL)
772				$mounted_objects[$row['object_id']] = TRUE;
773		}
774
775		$record['isDeletable'] = (count ($rows) || count ($mounted_objects)) ? FALSE : TRUE;
776		$record['mountedObjects'] = array_keys ($mounted_objects);
777		break;
778	case 'vst':
779		$record['rules'] = array();
780		$record['switches'] = array();
781		$result = usePreparedSelectBlade
782		(
783			'SELECT rule_no, port_pcre, port_role, wrt_vlans, description ' .
784			'FROM VLANSTRule WHERE vst_id = ? ORDER BY rule_no',
785			array ($record['id'])
786		);
787		while ($row = $result->fetch (PDO::FETCH_ASSOC))
788			$record['rules'][$row['rule_no']] = $row;
789		unset ($result);
790		$result = usePreparedSelectBlade ('SELECT object_id, domain_id FROM VLANSwitch WHERE template_id = ?', array ($record['id']));
791		while ($row = $result->fetch (PDO::FETCH_ASSOC))
792			$record['switches'][$row['object_id']] = $row;
793		break;
794	case 'ipvs':
795		$record['ports'] = array();
796		$record['vips'] = array();
797		$result = usePreparedSelectBlade ("SELECT proto, vport, vsconfig, rsconfig FROM VSPorts WHERE vs_id = ?", array ($record['id']));
798		while ($row = $result->fetch (PDO::FETCH_ASSOC))
799		{
800			$row['vsconfig'] = dos2unix ($row['vsconfig']);
801			$row['rsconfig'] = dos2unix ($row['rsconfig']);
802			$record['ports'][] = $row;
803		}
804		unset ($result);
805		$result = usePreparedSelectBlade ("SELECT vip, vsconfig, rsconfig FROM VSIPs WHERE vs_id = ?", array ($record['id']));
806		while ($row = $result->fetch (PDO::FETCH_ASSOC))
807		{
808			$row['vsconfig'] = dos2unix ($row['vsconfig']);
809			$row['rsconfig'] = dos2unix ($row['rsconfig']);
810			$record['vips'][] = $row;
811		}
812		unset ($result);
813		break;
814	default:
815	}
816}
817
818// is called by spotEntity and listCells.
819// replaces ['8021q'] text value in cell by an array with 'domain_id' and 'vlan_id' subkeys
820// also sets ['vlanc'] cell key to the binded vlans count.
821function processIPNetVlans (&$cell)
822{
823	if (empty ($cell['8021q']))
824		$cell['8021q'] = array();
825	else
826	{
827		$ck_list = explode (',', $cell['8021q']);
828		$cell['8021q'] = array();
829		foreach ($ck_list as $vlan_ck)
830		{
831			list ($domain_id, $vlan_id) = decodeVLANCK ($vlan_ck);
832			$cell['8021q'][] = array ('domain_id' => $domain_id, 'vlan_id' => $vlan_id);
833		}
834	}
835	$cell['vlanc'] = count ($cell['8021q']);
836}
837
838function fetchPortList ($sql_where_clause, $query_params = array())
839{
840	// Heredoc, not nowdoc!
841	$query = <<<"END"
842SELECT
843	Port.id,
844	Port.name,
845	Port.object_id,
846	Object.name AS object_name,
847	Port.l2address,
848	Port.label,
849	Port.reservation_comment,
850	Port.iif_id,
851	Port.type AS oif_id,
852	(SELECT PortInnerInterface.iif_name FROM PortInnerInterface WHERE PortInnerInterface.id = Port.iif_id) AS iif_name,
853	(SELECT PortOuterInterface.oif_name FROM PortOuterInterface WHERE PortOuterInterface.id = Port.type) AS oif_name,
854	IF(la.porta, la.cable, lb.cable) AS cableid,
855	IF(la.porta, pa.id, pb.id) AS remote_id,
856	IF(la.porta, pa.name, pb.name) AS remote_name,
857	IF(la.porta, pa.object_id, pb.object_id) AS remote_object_id,
858	IF(la.porta, oa.name, ob.name) AS remote_object_name,
859	IF(la.porta, oa.objtype_id, ob.objtype_id) AS remote_object_tid,
860	(SELECT COUNT(*) FROM PortLog WHERE PortLog.port_id = Port.id) AS log_count,
861	PortLog.user,
862	UNIX_TIMESTAMP(PortLog.date) as time
863FROM
864	Port
865	INNER JOIN Object ON Port.object_id = Object.id
866	LEFT JOIN Link AS la ON la.porta = Port.id
867	LEFT JOIN Port AS pa ON pa.id = la.portb
868	LEFT JOIN Object AS oa ON pa.object_id = oa.id
869	LEFT JOIN Link AS lb on lb.portb = Port.id
870	LEFT JOIN Port AS pb ON pb.id = lb.porta
871	LEFT JOIN Object AS ob ON pb.object_id = ob.id
872	LEFT JOIN PortLog ON PortLog.id = (SELECT id FROM PortLog WHERE PortLog.port_id = Port.id ORDER BY date DESC LIMIT 1)
873WHERE
874	$sql_where_clause
875END;
876
877	$result = usePreparedSelectBlade ($query, $query_params);
878	$ret = array();
879	while ($row = $result->fetch (PDO::FETCH_ASSOC))
880	{
881		$row['l2address'] = l2addressFromDatabase ($row['l2address']);
882		$row['linked'] = isset ($row['remote_id']) ? 1 : 0;
883
884		// last changed log
885		$row['last_log'] = array();
886		if ($row['log_count'])
887		{
888			$row['last_log']['user'] = $row['user'];
889			$row['last_log']['time'] = $row['time'];
890		}
891		unset ($row['user']);
892		unset ($row['time']);
893
894		$ret[] = $row;
895	}
896	return $ret;
897}
898
899function getObjectPortsAndLinks ($object_id, $sorted = TRUE)
900{
901	$ret = fetchPortList ("Port.object_id = ?", array ($object_id));
902	if ($sorted)
903		$ret = sortPortList ($ret, TRUE);
904	return $ret;
905}
906
907// This function provides data for replaceObjectPorts() and requires only two tables locked.
908function getObjectPortsAndLinksTerse ($object_id)
909{
910	$result = usePreparedSelectBlade
911	(
912		'SELECT id, name, iif_id, type AS oif_id, label, l2address, reservation_comment, ' .
913		'(SELECT COUNT(*) FROM Link WHERE porta = Port.id OR portb = Port.id) AS link_count ' .
914		'FROM Port WHERE object_id = ?',
915		array ($object_id)
916	);
917	return $result->fetchAll (PDO::FETCH_ASSOC);
918}
919
920// Fetch the object type via SQL.
921// spotEntity cannot be used because it references RackObject, which doesn't suit Racks, Rows, or Locations.
922function getObjectType ($object_id)
923{
924	$result = usePreparedSelectBlade ('SELECT objtype_id from Object WHERE id = ?', array ($object_id));
925	return $result->fetchColumn ();
926}
927
928// If the given name is used by any object other than the current object,
929// raise an exception.  Validation is bypassed for certain object types
930// where duplicates are acceptable.
931// NOTE: This could be enforced more strictly at the database level using triggers.
932function checkObjectNameUniqueness ($name, $type_id, $object_id = 0)
933{
934	// Some object types do not need unique names
935	// 1560 - Rack
936	// 1561 - Row
937	$dupes_allowed = array (1560, 1561);
938	if (in_array ($type_id, $dupes_allowed))
939		return;
940
941	$result = usePreparedSelectBlade
942	(
943		'SELECT COUNT(*) FROM Object WHERE name = ? AND id != ?',
944		array ($name, $object_id)
945	);
946	$row = $result->fetch (PDO::FETCH_NUM);
947	if ($row[0] != 0)
948		throw new InvalidRequestArgException ('name', $name, 'An object with that name already exists');
949}
950
951function commitAddObject ($new_name, $new_label, $new_type_id, $new_asset_no, $taglist = array())
952{
953	checkObjectNameUniqueness ($new_name, $new_type_id);
954	usePreparedInsertBlade
955	(
956		'Object',
957		array
958		(
959			'name' => nullIfEmptyStr ($new_name),
960			'label' => nullIfEmptyStr ($new_label),
961			'objtype_id' => $new_type_id,
962			'asset_no' => nullIfEmptyStr ($new_asset_no),
963		)
964	);
965	$object_id = lastInsertID();
966	switch ($new_type_id)
967	{
968		case 1560:
969			$realm = 'rack';
970			break;
971		case 1561:
972			$realm = 'row';
973			break;
974		case 1562:
975			$realm = 'location';
976			break;
977		default:
978			$realm = 'object';
979	}
980	lastCreated ($realm, $object_id);
981
982	// Store any tags before executeAutoPorts() calls spotEntity() and populates the cache.
983	produceTagsForNewRecord ($realm, $taglist, $object_id);
984	// Do AutoPorts magic
985	if ($realm == 'object')
986		executeAutoPorts ($object_id);
987	recordObjectHistory ($object_id);
988	return $object_id;
989}
990
991function commitRenameObject ($object_id, $new_name)
992{
993	$type_id = getObjectType ($object_id);
994	checkObjectNameUniqueness ($new_name, $type_id, $object_id);
995	usePreparedUpdateBlade
996	(
997		'Object',
998		array
999		(
1000			'name' => nullIfEmptyStr ($new_name),
1001		),
1002		array
1003		(
1004			'id' => $object_id
1005		)
1006	);
1007	recordObjectHistory ($object_id);
1008}
1009
1010function commitUpdateObject ($object_id, $new_name, $new_label, $new_has_problems, $new_asset_no, $new_comment)
1011{
1012	$set_columns = array
1013	(
1014		'name' => nullIfEmptyStr ($new_name),
1015		'label' => nullIfEmptyStr ($new_label),
1016		'has_problems' => $new_has_problems == '' ? 'no' : $new_has_problems,
1017		'asset_no' => nullIfEmptyStr ($new_asset_no),
1018		'comment' => nullIfEmptyStr ($new_comment),
1019	);
1020	$override = callHook('commitUpdateObjectBefore_hook', $object_id, $set_columns);
1021	if (is_array ($override))
1022		$set_columns = $override;
1023	$type_id = getObjectType ($object_id);
1024	checkObjectNameUniqueness ($new_name, $type_id, $object_id);
1025	usePreparedUpdateBlade
1026	(
1027		'Object',
1028		$set_columns,
1029		array
1030		(
1031			'id' => $object_id
1032		)
1033	);
1034	recordObjectHistory ($object_id);
1035	callHook ('commitUpdateObjectAfter_hook', $object_id);
1036}
1037
1038function compare_name ($a, $b)
1039{
1040	return strnatcmp($a['name'], $b['name']);
1041}
1042
1043// find either parents or children of a record
1044function getEntityRelatives ($type, $entity_type, $entity_id)
1045{
1046	$ret = array();
1047
1048	if ($type == 'parents')
1049	{
1050		// searching for parents
1051		$sql =
1052			'SELECT id, parent_entity_type AS entity_type, parent_entity_id AS entity_id FROM EntityLink ' .
1053			'WHERE child_entity_type = ? AND child_entity_id = ?';
1054	}
1055	else
1056	{
1057		// searching for children
1058		$sql =
1059			'SELECT id, child_entity_type AS entity_type, child_entity_id AS entity_id FROM EntityLink ' .
1060			'WHERE parent_entity_type = ? AND parent_entity_id = ?';
1061	}
1062
1063	$result = usePreparedSelectBlade ($sql, array ($entity_type, $entity_id));
1064	while ($row = $result->fetch (PDO::FETCH_ASSOC))
1065		$ret[$row['id']] = array(
1066			'entity_type' => $row['entity_type'],
1067			'entity_id' => $row['entity_id'],
1068		);
1069
1070	return $ret;
1071}
1072
1073function getParents ($entity, $result_realm = NULL)
1074{
1075	return getRelatives ($entity, 'parents', $result_realm);
1076}
1077
1078function getChildren ($entity, $result_realm = NULL)
1079{
1080	return getRelatives ($entity, 'children', $result_realm);
1081}
1082
1083function getRelatives ($entity, $type, $result_realm = NULL)
1084{
1085	$ret = array();
1086	foreach (getEntityRelatives ($type, $entity['realm'], $entity['id']) as $link_id => $struct)
1087		if (! isset ($result_realm) || $result_realm == $struct['entity_type'])
1088			$ret[$link_id] = spotEntity ($struct['entity_type'], $struct['entity_id']);
1089	return $ret;
1090}
1091
1092// This function is recursive and returns only object IDs.
1093function getObjectContentsList ($object_id, $children = array ())
1094{
1095	$self = __FUNCTION__;
1096	$result = usePreparedSelectBlade
1097	(
1098		'SELECT child_entity_id FROM EntityLink ' .
1099		'WHERE parent_entity_type = "object" AND child_entity_type = "object" AND parent_entity_id = ?',
1100		array ($object_id)
1101	);
1102	$rows = $result->fetchAll (PDO::FETCH_ASSOC);
1103	unset ($result);
1104	foreach ($rows as $row)
1105	{
1106		if (in_array ($row['child_entity_id'], $children))
1107			throw new RackTablesError ("Circular reference for object ${object_id}", RackTablesError::INTERNAL);
1108		$children[] = $row['child_entity_id'];
1109		$children = array_unique (array_merge ($children, $self ($row['child_entity_id'], $children)));
1110	}
1111	return $children;
1112}
1113
1114// This function is recursive and returns only location IDs.
1115function getLocationChildrenList ($location_id, $children = array ())
1116{
1117	$self = __FUNCTION__;
1118	$result = usePreparedSelectBlade ('SELECT id FROM Location WHERE parent_id = ?', array ($location_id));
1119	$rows = $result->fetchAll (PDO::FETCH_ASSOC);
1120	unset ($result);
1121	foreach ($rows as $row)
1122	{
1123		if (in_array ($row['id'], $children))
1124			throw new RackTablesError ("Circular reference for location ${location_id}", RackTablesError::INTERNAL);
1125		$children[] = $row['id'];
1126		$children = array_unique (array_merge ($children, $self ($row['id'], $children)));
1127	}
1128	return $children;
1129}
1130
1131// DEPRECATED: use getTagDescendents() instead
1132// This function is recursive and returns only tag IDs.
1133function getTagChildrenList ($tag_id, $children = array ())
1134{
1135	$self = __FUNCTION__;
1136	$result = usePreparedSelectBlade ('SELECT id FROM TagTree WHERE parent_id = ?', array ($tag_id));
1137	$rows = $result->fetchAll (PDO::FETCH_ASSOC);
1138	unset ($result);
1139	foreach ($rows as $row)
1140	{
1141		if (in_array ($row['id'], $children))
1142			throw new RackTablesError ("Circular reference for tag ${tag_id}", RackTablesError::INTERNAL);
1143		$children[] = $row['id'];
1144		$children = array_unique (array_merge ($children, $self ($row['id'], $children)));
1145	}
1146	return $children;
1147}
1148
1149function commitLinkEntities ($parent_entity_type, $parent_entity_id, $child_entity_type, $child_entity_id)
1150{
1151	// a location's parent may not be one of its children
1152	if
1153	(
1154		$parent_entity_type == 'location' &&
1155		$child_entity_type == 'location' &&
1156		in_array ($parent_entity_id, getLocationChildrenList ($child_entity_id))
1157	)
1158		throw new RackTablesError ("Circular reference for location ${parent_entity_id}", RackTablesError::INTERNAL);
1159
1160	// an object's container may not be one of its contained objects
1161	if
1162	(
1163		$parent_entity_type == 'object' &&
1164		$child_entity_type == 'object' &&
1165		in_array ($parent_entity_id, getObjectContentsList ($child_entity_id))
1166	)
1167		throw new RackTablesError ("Circular reference for object ${parent_entity_id}", RackTablesError::INTERNAL);
1168
1169	usePreparedInsertBlade
1170	(
1171		'EntityLink',
1172		array
1173		(
1174			'parent_entity_type' => $parent_entity_type,
1175			'parent_entity_id' => $parent_entity_id,
1176			'child_entity_type' => $child_entity_type,
1177			'child_entity_id' => $child_entity_id,
1178		)
1179	);
1180}
1181
1182function commitUpdateEntityLink
1183(
1184	$old_parent_entity_type, $old_parent_entity_id, $old_child_entity_type, $old_child_entity_id,
1185	$new_parent_entity_type, $new_parent_entity_id, $new_child_entity_type, $new_child_entity_id
1186)
1187{
1188	// a location's parent may not be one of its children
1189	if
1190	(
1191		$new_parent_entity_type == 'location' &&
1192		$new_child_entity_type == 'location' &&
1193		in_array ($new_parent_entity_id, getLocationChildrenList ($new_child_entity_id))
1194	)
1195		throw new RackTablesError ("Circular reference for location ${new_parent_entity_id}", RackTablesError::INTERNAL);
1196
1197	// an object's container may not be one of its contained objects
1198	if
1199	(
1200		$new_parent_entity_type == 'object' &&
1201		$new_child_entity_type == 'object' &&
1202		in_array ($new_parent_entity_id, getObjectContentsList ($new_child_entity_id))
1203	)
1204		throw new RackTablesError ("Circular reference for object ${new_parent_entity_id}", RackTablesError::INTERNAL);
1205
1206	usePreparedUpdateBlade
1207	(
1208		'EntityLink',
1209		array
1210		(
1211			'parent_entity_type' => $new_parent_entity_type,
1212			'parent_entity_id' => $new_parent_entity_id,
1213			'child_entity_type' => $new_child_entity_type,
1214			'child_entity_id' => $new_child_entity_id
1215		),
1216		array
1217		(
1218			'parent_entity_type' => $old_parent_entity_type,
1219			'parent_entity_id' => $old_parent_entity_id,
1220			'child_entity_type' => $old_child_entity_type,
1221			'child_entity_id' => $old_child_entity_id
1222		)
1223	);
1224}
1225
1226function commitUnlinkEntities ($parent_entity_type, $parent_entity_id, $child_entity_type, $child_entity_id)
1227{
1228	usePreparedDeleteBlade
1229	(
1230		'EntityLink',
1231		array
1232		(
1233			'parent_entity_type' => $parent_entity_type,
1234			'parent_entity_id' => $parent_entity_id,
1235			'child_entity_type' => $child_entity_type,
1236			'child_entity_id' => $child_entity_id
1237		)
1238	);
1239}
1240
1241function commitUnlinkEntitiesByLinkID ($link_id)
1242{
1243	usePreparedDeleteBlade ('EntityLink', array ('id' => $link_id));
1244}
1245
1246// return VM clusters and corresponding stats
1247//	- number of hypervisors
1248//	- number of resource pools
1249//	- number of VMs whose parent is the cluster itself
1250//	- number of VMs whose parent is one of the resource pools in the cluster
1251function getVMClusterSummary ()
1252{
1253	$query = <<<'END'
1254SELECT
1255	O.id,
1256	O.name,
1257	(SELECT COUNT(*) FROM EntityLink EL
1258		LEFT JOIN Object O_H ON EL.child_entity_id = O_H.id
1259		LEFT JOIN AttributeValue AV ON O_H.id = AV.object_id
1260		WHERE EL.parent_entity_type = 'object'
1261		AND EL.child_entity_type = 'object'
1262		AND EL.parent_entity_id = O.id
1263		AND O_H.objtype_id = 4
1264		AND AV.attr_id = 26
1265		AND AV.uint_value = 1501) AS hypervisors,
1266	(SELECT COUNT(*) FROM EntityLink EL
1267		LEFT JOIN Object O_RP ON EL.child_entity_id = O_RP.id
1268		WHERE EL.parent_entity_type = 'object'
1269		AND EL.child_entity_type = 'object'
1270		AND EL.parent_entity_id = O.id
1271		AND O_RP.objtype_id = 1506) AS resource_pools,
1272	(SELECT COUNT(*) FROM EntityLink EL
1273		LEFT JOIN Object O_C_VM ON EL.child_entity_id = O_C_VM.id
1274		WHERE EL.parent_entity_type = 'object'
1275		AND EL.child_entity_type = 'object'
1276		AND EL.parent_entity_id = O.id
1277		AND O_C_VM.objtype_id = 1504) AS cluster_vms,
1278	(SELECT COUNT(*) FROM EntityLink EL
1279		LEFT JOIN Object O_RP_VM ON EL.child_entity_id = O_RP_VM.id
1280		WHERE EL.parent_entity_type = 'object'
1281		AND EL.child_entity_type = 'object'
1282		AND EL.parent_entity_id IN
1283			(SELECT child_entity_id FROM EntityLink EL
1284				LEFT JOIN Object O_RP ON EL.child_entity_id = O_RP.id
1285				WHERE EL.parent_entity_type = 'object'
1286				AND EL.child_entity_type = 'object'
1287				AND EL.parent_entity_id = O.id
1288				AND O_RP.objtype_id = 1506)
1289		AND O_RP_VM.objtype_id = 1504) AS resource_pool_vms
1290FROM Object O
1291WHERE O.objtype_id = 1505
1292ORDER BY O.name
1293END;
1294	$result = usePreparedSelectBlade ($query);
1295	return $result->fetchAll (PDO::FETCH_ASSOC);
1296}
1297
1298function getVMResourcePoolSummary ()
1299{
1300	$result = usePreparedSelectBlade
1301	(
1302		"SELECT O.id, O.name, " .
1303		"(SELECT O_C.id " .
1304		"FROM EntityLink EL " .
1305		"LEFT JOIN Object O_C ON EL.parent_entity_id = O_C.id " .
1306		"WHERE EL.child_entity_id = O.id " .
1307		"AND EL.parent_entity_type = 'object' " .
1308		"AND EL.child_entity_type = 'object' " .
1309		"AND O_C.objtype_id = 1505 LIMIT 1) AS cluster_id, " .
1310		"(SELECT O_C.name " .
1311		"FROM EntityLink EL " .
1312		"LEFT JOIN Object O_C ON EL.parent_entity_id = O_C.id " .
1313		"WHERE EL.child_entity_id = O.id " .
1314		"AND EL.parent_entity_type = 'object' " .
1315		"AND EL.child_entity_type = 'object' " .
1316		"AND O_C.objtype_id = 1505 LIMIT 1) AS cluster_name, " .
1317		"(SELECT COUNT(*) FROM EntityLink EL " .
1318		"LEFT JOIN Object O_VM ON EL.child_entity_id = O_VM.id " .
1319		"WHERE EL.parent_entity_type = 'object' " .
1320		"AND EL.child_entity_type = 'object' " .
1321		"AND EL.parent_entity_id = O.id " .
1322		"AND O_VM.objtype_id = 1504) AS VMs " .
1323		"FROM Object O " .
1324		"WHERE O.objtype_id = 1506 " .
1325		"ORDER BY O.name"
1326	);
1327	return $result->fetchAll (PDO::FETCH_ASSOC);
1328}
1329
1330function getVMHypervisorSummary ()
1331{
1332	$result = usePreparedSelectBlade
1333	(
1334		"SELECT O.id, O.name, " .
1335		"(SELECT O_C.id " .
1336		"FROM EntityLink EL " .
1337		"LEFT JOIN Object O_C ON EL.parent_entity_id = O_C.id " .
1338		"WHERE EL.child_entity_id = O.id " .
1339		"AND EL.parent_entity_type = 'object' " .
1340		"AND EL.child_entity_type = 'object' " .
1341		"AND O_C.objtype_id = 1505 LIMIT 1) AS cluster_id, " .
1342		"(SELECT O_C.name " .
1343		"FROM EntityLink EL " .
1344		"LEFT JOIN Object O_C ON EL.parent_entity_id = O_C.id " .
1345		"WHERE EL.child_entity_id = O.id " .
1346		"AND EL.parent_entity_type = 'object' " .
1347		"AND EL.child_entity_type = 'object' " .
1348		"AND O_C.objtype_id = 1505 LIMIT 1) AS cluster_name, " .
1349		"(SELECT COUNT(*) FROM EntityLink EL " .
1350		"LEFT JOIN Object O_VM ON EL.child_entity_id = O_VM.id " .
1351		"WHERE EL.parent_entity_type = 'object' " .
1352		"AND EL.child_entity_type = 'object' " .
1353		"AND EL.parent_entity_id = O.id " .
1354		"AND O_VM.objtype_id = 1504) AS VMs " .
1355		"FROM Object O " .
1356		"LEFT JOIN AttributeValue AV ON O.id = AV.object_id " .
1357		"WHERE O.objtype_id = 4 " .
1358		"AND AV.attr_id = 26 " .
1359		"AND AV.uint_value = 1501 " .
1360		"ORDER BY O.name"
1361	);
1362	return $result->fetchAll (PDO::FETCH_ASSOC);
1363}
1364
1365function getVMSwitchSummary ()
1366{
1367	$result = usePreparedSelectBlade
1368	(
1369		"SELECT O.id, O.name " .
1370		"FROM Object O " .
1371		"WHERE O.objtype_id = 1507 " .
1372		"ORDER BY O.name"
1373	);
1374	return $result->fetchAll (PDO::FETCH_ASSOC);
1375}
1376
1377// Remove file links related to the entity, but leave the entity and file(s) intact.
1378function releaseFiles ($entity_realm, $entity_id)
1379{
1380	usePreparedDeleteBlade ('FileLink', array ('entity_type' => $entity_realm, 'entity_id' => $entity_id));
1381}
1382
1383// There are times when you want to delete all traces of an object
1384function commitDeleteObject ($object_id)
1385{
1386	// Reset most of stuff
1387	commitResetObject ($object_id);
1388	// Object itself
1389	usePreparedDeleteBlade ('Object', array ('id' => $object_id));
1390	// Dangling links
1391	usePreparedExecuteBlade
1392	(
1393		'DELETE FROM EntityLink WHERE ' .
1394		"(parent_entity_type IN ('rack', 'row', 'location') AND parent_entity_id = ?) OR " .
1395		"(child_entity_type IN ('rack', 'row', 'location') AND child_entity_id = ?)",
1396		array ($object_id, $object_id)
1397	);
1398}
1399
1400function commitResetObject ($object_id)
1401{
1402	releaseFiles ('object', $object_id);
1403	destroyTagsForEntity ('object', $object_id);
1404	usePreparedDeleteBlade ('IPv4LB', array ('object_id' => $object_id));
1405	usePreparedDeleteBlade ('IPv4Allocation', array ('object_id' => $object_id));
1406	usePreparedDeleteBlade ('IPv6Allocation', array ('object_id' => $object_id));
1407	usePreparedDeleteBlade ('IPv4NAT', array ('object_id' => $object_id));
1408	// Parent-child relationships
1409	usePreparedExecuteBlade
1410	(
1411		'DELETE FROM EntityLink WHERE ' .
1412		"(parent_entity_type = 'object' AND parent_entity_id = ?) OR (child_entity_type = 'object' AND child_entity_id = ?)",
1413		array ($object_id, $object_id)
1414	);
1415	// Rack space
1416	usePreparedExecuteBlade ('DELETE FROM Atom WHERE molecule_id IN (SELECT new_molecule_id FROM MountOperation WHERE object_id = ?)', array ($object_id));
1417	usePreparedExecuteBlade ('DELETE FROM Molecule WHERE id IN (SELECT new_molecule_id FROM MountOperation WHERE object_id = ?)', array ($object_id));
1418	usePreparedDeleteBlade ('MountOperation', array ('object_id' => $object_id));
1419	usePreparedDeleteBlade ('RackSpace', array ('object_id' => $object_id));
1420	// 802.1Q
1421	usePreparedDeleteBlade ('PortVLANMode', array ('object_id' => $object_id));
1422	usePreparedDeleteBlade ('PortNativeVLAN', array ('object_id' => $object_id));
1423	usePreparedDeleteBlade ('PortAllowedVLAN', array ('object_id' => $object_id));
1424	usePreparedDeleteBlade ('CachedPVM', array ('object_id' => $object_id));
1425	usePreparedDeleteBlade ('VLANSwitch', array ('object_id' => $object_id));
1426	// SLB
1427	usePreparedDeleteBlade ('IPv4LB', array ('object_id' => $object_id));
1428	usePreparedDeleteBlade ('VSEnabledIPs', array ('object_id' => $object_id));
1429	usePreparedDeleteBlade ('VSEnabledPorts', array ('object_id' => $object_id));
1430	// Ports & links
1431	usePreparedDeleteBlade ('Port', array ('object_id' => $object_id));
1432	// CN
1433	usePreparedUpdateBlade ('Object', array ('name' => NULL, 'label' => ''), array ('id' => $object_id));
1434	// FQDN
1435	commitUpdateAttrValue ($object_id, 3, "");
1436	// log history
1437	recordObjectHistory ($object_id);
1438	# Do an additional reset if needed
1439	callHook ('resetObject_hook', $object_id);
1440}
1441
1442function commitUpdateRack ($rack_id, $new_row_id, $new_name, $new_height, $new_has_problems, $new_asset_no, $new_comment)
1443{
1444	// Can't shrink a rack if rows being deleted contain mounted objects
1445	$check_result = usePreparedSelectBlade ('SELECT COUNT(*) AS count FROM RackSpace WHERE rack_id = ? AND unit_no > ?', array ($rack_id, $new_height));
1446	$check_row = $check_result->fetch (PDO::FETCH_ASSOC);
1447	unset ($check_result);
1448	if ($check_row['count'] > 0)
1449		throw new InvalidArgException ('new_height', $new_height, 'Cannot shrink rack, objects are still mounted there');
1450
1451	// Determine if the row changed
1452	$old_rack = spotEntity ('rack', $rack_id);
1453	$old_row_id = $old_rack['row_id'];
1454	if ($old_row_id != $new_row_id)
1455	{
1456		// Move it to the specified row
1457		usePreparedUpdateBlade
1458		(
1459			'EntityLink',
1460			array ('parent_entity_id' => $new_row_id),
1461			array ('child_entity_type' => 'rack', 'child_entity_id' => $rack_id)
1462		);
1463
1464		// Set the sort_order attribute so it's placed at the end of the new row
1465		$rowInfo = getRowInfo ($new_row_id);
1466		usePreparedUpdateBlade
1467		(
1468			'AttributeValue',
1469			array ('uint_value' => $rowInfo['count']),
1470			array ('object_id' => $rack_id, 'attr_id' => 29)
1471		);
1472
1473		// Reset the sort order of the old row
1474		resetRackSortOrder ($old_row_id);
1475	}
1476
1477	// Update the height
1478	commitUpdateAttrValue ($rack_id, 27, $new_height);
1479
1480	// Update the rack
1481	commitUpdateObject ($rack_id, $new_name, NULL, $new_has_problems, $new_asset_no, $new_comment);
1482}
1483
1484// Unmount all objects from the rack
1485function commitCleanRack ($rack_id)
1486{
1487	$rack = spotEntity ('rack', $rack_id);
1488	foreach (getChildren ($rack, 'object') as $child)
1489		commitUnlinkEntities ('rack', $rack_id, 'object', $child['id']);
1490	usePreparedDeleteBlade ('RackSpace', array ('rack_id' => $rack_id));
1491	usePreparedDeleteBlade ('RackThumbnail', array ('rack_id' => $rack_id));
1492}
1493
1494// Drop the rack
1495function commitDeleteRack ($rack_id)
1496{
1497	$rack = spotEntity ('rack', $rack_id);
1498	releaseFiles ('rack', $rack_id);
1499	destroyTagsForEntity ('rack', $rack_id);
1500	usePreparedDeleteBlade ('RackSpace', array ('rack_id' => $rack_id));
1501	commitDeleteObject ($rack_id);
1502	resetRackSortOrder ($rack['row_id']);
1503}
1504
1505// Drop the row with all racks inside
1506function commitDeleteRow ($row_id)
1507{
1508	$racks = getRacks ($row_id);
1509	foreach ($racks as $rack)
1510		commitDeleteRack ($rack['id']);
1511	commitDeleteObject ($row_id);
1512}
1513
1514// Returns mounted devices count in all racks inside the specified row
1515function getRowMountsCount ($row_id)
1516{
1517	$query =<<<'END'
1518SELECT COUNT(*) FROM (
1519	SELECT object_id FROM RackSpace rs LEFT JOIN EntityLink el ON (rs.rack_id = el.child_entity_id)
1520	WHERE
1521		rs.object_id IS NOT NULL AND
1522		el.parent_entity_id = ? AND el.parent_entity_type = "row" AND el.child_entity_type = "rack"
1523	UNION
1524	SELECT el1.child_entity_id object_id FROM EntityLink el1 LEFT JOIN EntityLink el2 ON (el1.parent_entity_id = el2.child_entity_id)
1525	WHERE
1526		el1.parent_entity_type = "rack" AND el1.child_entity_type = "object" AND
1527		el2.parent_entity_id = ? AND el2.parent_entity_type = "row" AND el2.child_entity_type = "rack"
1528) x
1529END;
1530	$result = usePreparedSelectBlade ($query, array ($row_id, $row_id));
1531	return $result->fetchColumn();
1532}
1533
1534// Returns mounted devices count in specified rack
1535function getRackMountsCount ($rack_id)
1536{
1537	$query =<<<'END'
1538SELECT COUNT(*) FROM (
1539	SELECT object_id FROM RackSpace WHERE object_id IS NOT NULL AND rack_id = ?
1540	UNION
1541	SELECT child_entity_id object_id FROM EntityLink WHERE
1542		parent_entity_id = ? AND parent_entity_type = "rack" AND child_entity_type = "object"
1543) x
1544END;
1545	$result = usePreparedSelectBlade ($query, array ($rack_id, $rack_id));
1546	return $result->fetchColumn();
1547}
1548
1549// Used when sort order is manually changed, and when a rack is moved or deleted
1550// Input is expected to be a pre-sorted array of rack IDs
1551function updateRackSortOrder ($racks)
1552{
1553	for ($i = 0; $i<count($racks); $i++)
1554	{
1555		usePreparedUpdateBlade
1556		(
1557			'AttributeValue',
1558			array ('uint_value' => $i+1),
1559			array ('object_id' => $racks[$i], 'attr_id' => 29)
1560		);
1561	}
1562}
1563
1564function resetRackSortOrder ($row_id)
1565{
1566	// Re-order the row's racks
1567	$racks = getRacks($row_id);
1568	$rack_ids = array ();
1569	foreach ($racks as $rack_id => $rackDetails)
1570		$rack_ids[] = $rack_id;
1571	updateRackSortOrder ($rack_ids);
1572}
1573
1574// This function builds a list of rack-unit-atom records assigned to
1575// the requested object.
1576function getMoleculeForObject ($object_id)
1577{
1578	$result = usePreparedSelectBlade
1579	(
1580		'SELECT rack_id, unit_no, atom FROM RackSpace ' .
1581		'WHERE state = "T" AND object_id = ? ORDER BY rack_id, unit_no, atom',
1582		array ($object_id)
1583	);
1584	return $result->fetchAll (PDO::FETCH_ASSOC);
1585}
1586
1587// This function builds a list of rack-unit-atom records for requested molecule.
1588function getMolecule ($mid)
1589{
1590	$result = usePreparedSelectBlade ('SELECT rack_id, unit_no, atom FROM Atom WHERE molecule_id = ?', array ($mid));
1591	return $result->fetchAll (PDO::FETCH_ASSOC);
1592}
1593
1594// returns exactly what is's named after
1595function lastInsertID ()
1596{
1597	$result = usePreparedSelectBlade ('select last_insert_id()');
1598	return $result->fetchColumn();
1599}
1600
1601// This function creates a new record in Molecule and number of linked
1602// R-U-A records in Atom.
1603function createMolecule ($molData)
1604{
1605	usePreparedExecuteBlade ('INSERT INTO Molecule VALUES()');
1606	$molecule_id = lastInsertID();
1607	foreach ($molData as $rua)
1608		usePreparedInsertBlade
1609		(
1610			'Atom',
1611			array
1612			(
1613				'molecule_id' => $molecule_id,
1614				'rack_id' => $rua['rack_id'],
1615				'unit_no' => $rua['unit_no'],
1616				'atom' => $rua['atom'],
1617			)
1618		);
1619	return $molecule_id;
1620}
1621
1622// History logger. This function assumes certain table naming convention and
1623// column design:
1624// - History table must have the same row set (w/o keys) plus one row named
1625//   'ctime' of type 'timestamp'.
1626function recordObjectHistory ($object_id)
1627{
1628	global $remote_username;
1629	usePreparedExecuteBlade
1630	(
1631		'INSERT INTO ObjectHistory ' .
1632		'(id, name, label, objtype_id, asset_no, has_problems, comment, ctime, user_name) ' .
1633		'SELECT id, name, label, objtype_id, asset_no, has_problems, comment, ' .
1634		'CURRENT_TIMESTAMP(), ? FROM Object WHERE id=?',
1635		array ($remote_username, $object_id)
1636	);
1637}
1638
1639function getRackspaceHistory ()
1640{
1641	$result = usePreparedSelectBlade
1642	(
1643		"SELECT id as mo_id, object_id as ro_id, ctime, comment, user_name FROM " .
1644		"MountOperation ORDER BY ctime DESC"
1645	);
1646	return $result->fetchAll (PDO::FETCH_ASSOC);
1647}
1648
1649// This function is used in renderRackspaceHistory()
1650function getOperationMolecules ($op_id)
1651{
1652	$result = usePreparedSelectBlade ('SELECT old_molecule_id, new_molecule_id FROM MountOperation WHERE id = ?', array ($op_id));
1653	// The result is a single row.
1654	$row = $result->fetch (PDO::FETCH_ASSOC);
1655	return array ($row['old_molecule_id'], $row['new_molecule_id']);
1656}
1657
1658function getResidentRackIDs ($object_id)
1659{
1660	$result = usePreparedSelectBlade
1661	(
1662		// Include racks that the object is directly mounted in
1663		"SELECT rack_id FROM RackSpace WHERE object_id = ? " .
1664		"UNION " .
1665		// Include racks that its parent is mounted in
1666		"SELECT RS.rack_id FROM RackSpace RS INNER JOIN EntityLink EL ON RS.object_id = EL.parent_entity_id AND EL.parent_entity_type = 'object' WHERE EL.child_entity_id = ? AND EL.child_entity_type = 'object' " .
1667		"UNION " .
1668		// and racks that it is 'Zero-U' mounted in
1669		"SELECT parent_entity_id AS rack_id FROM EntityLink WHERE parent_entity_type = 'rack' AND child_entity_type = 'object' AND child_entity_id = ? " .
1670		'ORDER BY rack_id', array ($object_id, $object_id, $object_id)
1671	);
1672	return $result->fetchAll (PDO::FETCH_COLUMN);
1673}
1674
1675function commitAddPort ($object_id, $port_name, $port_type_id, $port_label, $port_l2address)
1676{
1677	global $dbxlink;
1678	$db_l2address = l2addressForDatabase ($port_l2address);
1679	list ($iif_id, $oif_id) = parsePortIIFOIF ($port_type_id);
1680	// The conditional table locking is less relevant now due to replaceObjectPorts().
1681	if ($db_l2address != '')
1682		$dbxlink->exec ('LOCK TABLES Port WRITE');
1683	try
1684	{
1685		assertUniqueL2Addresses (array ($db_l2address), $object_id);
1686		$ret = commitAddPortReal ($object_id, $port_name, $iif_id, $oif_id, $port_label, $db_l2address);
1687	}
1688	catch (Exception $e)
1689	{
1690		if ($db_l2address != '')
1691			$dbxlink->exec ('UNLOCK TABLES');
1692		throw $e;
1693	}
1694	if ($db_l2address != '')
1695		$dbxlink->exec ('UNLOCK TABLES');
1696	return $ret;
1697}
1698
1699// Having the call to assertUniqueL2Addresses() in this function would break things because
1700// if the constraint check fails for any port the whole "transaction" needs to be rolled
1701// back. Thus the calling function must call assertUniqueL2Addresses() for all involved ports
1702// first and only then start making any calls to this function.
1703function commitAddPortReal ($object_id, $port_name, $iif_id, $oif_id, $port_label, $db_l2address)
1704{
1705	usePreparedInsertBlade
1706	(
1707		'Port',
1708		array
1709		(
1710			'name' => $port_name,
1711			'object_id' => $object_id,
1712			'label' => nullIfEmptyStr ($port_label),
1713			'iif_id' => $iif_id,
1714			'type' => $oif_id,
1715			'l2address' => nullIfEmptyStr ($db_l2address),
1716		)
1717	);
1718	lastCreated ('port', lastInsertID());
1719	return lastInsertID();
1720}
1721
1722function getPortReservationComment ($port_id, $extrasql = '')
1723{
1724	$result = usePreparedSelectBlade ("SELECT reservation_comment FROM Port WHERE id = ? $extrasql", array ($port_id));
1725	return $result->fetchColumn();
1726}
1727
1728function commitUpdatePort ($object_id, $port_id, $port_name, $port_type_id, $port_label, $port_l2address, $port_reservation_comment)
1729{
1730	global $dbxlink;
1731	$db_l2address = l2addressForDatabase ($port_l2address);
1732	list ($iif_id, $oif_id) = parsePortIIFOIF ($port_type_id);
1733	if ($db_l2address != '')
1734		$dbxlink->exec ('LOCK TABLES Port WRITE, PortLog WRITE');
1735	try
1736	{
1737		assertUniqueL2Addresses (array ($db_l2address), $object_id);
1738		commitUpdatePortReal ($object_id, $port_id, $port_name, $iif_id, $oif_id, $port_label, $db_l2address, $port_reservation_comment);
1739	}
1740	catch (Exception $e)
1741	{
1742		if ($db_l2address != '')
1743			$dbxlink->exec ('UNLOCK TABLES');
1744		throw $e;
1745	}
1746	if ($db_l2address != '')
1747		$dbxlink->exec ('UNLOCK TABLES');
1748}
1749
1750// The comment about commitAddPortReal() also applies here.
1751function commitUpdatePortReal ($object_id, $port_id, $port_name, $iif_id, $oif_id, $port_label, $db_l2address, $port_reservation_comment)
1752{
1753	$old_reservation_comment = getPortReservationComment ($port_id);
1754	$port_reservation_comment = nullIfEmptyStr ($port_reservation_comment);
1755	usePreparedUpdateBlade
1756	(
1757		'Port',
1758		array
1759		(
1760			'name' => $port_name,
1761			'iif_id' => $iif_id,
1762			'type' => $oif_id,
1763			'label' => nullIfEmptyStr ($port_label),
1764			'reservation_comment' => $port_reservation_comment,
1765			'l2address' => nullIfEmptyStr ($db_l2address),
1766		),
1767		array
1768		(
1769			'id' => $port_id,
1770			'object_id' => $object_id
1771		)
1772	);
1773	if ($old_reservation_comment !== $port_reservation_comment)
1774		addPortLogEntry ($port_id, sprintf ("Reservation changed from '%s' to '%s'", $old_reservation_comment, $port_reservation_comment));
1775}
1776
1777function commitUpdatePortComment ($port_id, $port_reservation_comment)
1778{
1779	global $dbxlink;
1780	$dbxlink->beginTransaction();
1781	$prev_comment = getPortReservationComment ($port_id, 'FOR UPDATE');
1782	$reservation_comment = nullIfEmptyStr ($port_reservation_comment);
1783	usePreparedUpdateBlade
1784	(
1785		'Port',
1786		array
1787		(
1788			'reservation_comment' => $reservation_comment,
1789		),
1790		array
1791		(
1792			'id' => $port_id,
1793		)
1794	);
1795	if ($prev_comment !== $reservation_comment)
1796		addPortLogEntry ($port_id, sprintf ("Reservation changed from '%s' to '%s'", $prev_comment, $reservation_comment));
1797	$dbxlink->commit();
1798}
1799
1800function commitUpdatePortOIF ($port_id, $port_type_id)
1801{
1802	usePreparedUpdateBlade
1803	(
1804		'Port',
1805		array ('type' => $port_type_id),
1806		array ('id' => $port_id)
1807	);
1808}
1809
1810function getAllIPv4Allocations ()
1811{
1812	$result = usePreparedSelectBlade
1813	(
1814		'SELECT object_id AS object_id, Object.name AS object_name, objtype_id, ' .
1815		'IPv4Allocation.name AS name, IPv4Allocation.type AS type, INET_NTOA(ip) AS ip '.
1816		'FROM IPv4Allocation JOIN Object ON id = object_id'
1817	);
1818	return $result->fetchAll (PDO::FETCH_ASSOC);
1819}
1820
1821function linkPorts ($porta, $portb, $cable = NULL)
1822{
1823	if ($porta == $portb)
1824		throw new InvalidArgException ('porta/portb', $porta, "Ports can't be the same");
1825
1826	$result = usePreparedSelectBlade
1827	(
1828		'SELECT COUNT(*) FROM Link WHERE porta IN (?,?) OR portb IN (?,?)',
1829		array ($porta, $portb, $porta, $portb)
1830	);
1831	if ($result->fetchColumn () != 0)
1832		throw new RTDatabaseError ("Port ${porta} or ${portb} is already linked");
1833	unset ($result);
1834
1835	$ret = usePreparedInsertBlade
1836	(
1837		'Link',
1838		array
1839		(
1840			'porta' => $porta,
1841			'portb' => $portb,
1842			'cable' => nullIfEmptyStr ($cable),
1843		)
1844	);
1845	usePreparedUpdateBlade ('Port', array ('reservation_comment' => NULL), array ('id' => array ($porta, $portb)));
1846
1847	// log new links
1848	$result = usePreparedSelectBlade
1849	(
1850		"SELECT Port.id, Port.name as port_name, Object.name as obj_name FROM Port " .
1851		"INNER JOIN Object ON Port.object_id = Object.id WHERE Port.id IN (?, ?)",
1852		array ($porta, $portb)
1853	);
1854	$rows = $result->fetchAll (PDO::FETCH_ASSOC);
1855	unset ($result);
1856	foreach ($rows as $row)
1857	{
1858		$pair_id = ($row['id'] == $porta ? $portb : $porta);
1859		addPortLogEntry ($pair_id, sprintf ("linked to %s %s", $row['obj_name'], $row['port_name']));
1860	}
1861	return $ret;
1862}
1863
1864function commitUpdatePortLink ($port_id, $cable = NULL)
1865{
1866	return usePreparedUpdateBlade
1867	(
1868		'Link',
1869		array ('cable' => nullIfEmptyStr ($cable)),
1870		array ('porta' => $port_id, 'portb' => $port_id),
1871		'OR'
1872	);
1873}
1874
1875function commitUnlinkPort ($port_id)
1876{
1877	// fetch and log existing link
1878	$result = usePreparedSelectBlade
1879	(
1880		"SELECT	pa.id AS id_a, pa.name AS port_name_a, oa.name AS obj_name_a, " .
1881		"pb.id AS id_b, pb.name AS port_name_b, ob.name AS obj_name_b " .
1882		"FROM " .
1883		"Link INNER JOIN Port pa ON pa.id = Link.porta " .
1884		"INNER JOIN Port pb ON pb.id = Link.portb " .
1885		"INNER JOIN RackObject oa ON pa.object_id = oa.id " .
1886		"INNER JOIN RackObject ob ON pb.object_id = ob.id " .
1887		"WHERE " .
1888		"Link.porta = ? OR Link.portb = ?",
1889		array ($port_id, $port_id)
1890	);
1891	$rows = $result->fetchAll (PDO::FETCH_ASSOC);
1892	unset ($result);
1893	foreach ($rows as $row)
1894	{
1895		addPortLogEntry ($row['id_a'], sprintf ("unlinked from %s %s", $row['obj_name_b'], $row['port_name_b']));
1896		addPortLogEntry ($row['id_b'], sprintf ("unlinked from %s %s", $row['obj_name_a'], $row['port_name_a']));
1897	}
1898
1899	// remove existing link
1900	return usePreparedDeleteBlade ('Link', array ('porta' => $port_id, 'portb' => $port_id), 'OR');
1901}
1902
1903function addPortLogEntry ($port_id, $message)
1904{
1905	global $disable_logging;
1906	if (isset ($disable_logging) && $disable_logging)
1907		return;
1908	global $remote_username;
1909	usePreparedExecuteBlade
1910	(
1911		"INSERT INTO PortLog (port_id, user, date, message) VALUES (?, ?, NOW(), ?)",
1912		array ($port_id, $remote_username, $message)
1913	);
1914}
1915
1916function addIPLogEntry ($ip_bin, $message)
1917{
1918	switch (strlen ($ip_bin))
1919	{
1920		case 4:  return addIPv4LogEntry ($ip_bin, $message);
1921		case 16: return addIPv6LogEntry ($ip_bin, $message);
1922		default: throw new InvalidArgException ('ip_bin', $ip_bin, "Invalid binary IP");
1923	}
1924}
1925
1926function addIPv4LogEntry ($ip_bin, $message)
1927{
1928	global $disable_logging;
1929	if (isset ($disable_logging) && $disable_logging)
1930		return;
1931	global $remote_username;
1932	usePreparedExecuteBlade
1933	(
1934		"INSERT INTO IPv4Log (ip, date, user, message) VALUES (?, NOW(), ?, ?)",
1935		array (ip4_bin2db ($ip_bin), $remote_username, $message)
1936	);
1937}
1938
1939function addIPv6LogEntry ($ip_bin, $message)
1940{
1941	global $disable_logging;
1942	if (isset ($disable_logging) && $disable_logging)
1943		return;
1944	global $remote_username;
1945	usePreparedExecuteBlade
1946	(
1947		"INSERT INTO IPv6Log (ip, date, user, message) VALUES (?, NOW(), ?, ?)",
1948		array ($ip_bin, $remote_username, $message)
1949	);
1950}
1951
1952function fetchIPLogEntry ($ip_bin)
1953{
1954	switch (strlen ($ip_bin))
1955	{
1956		case 4:  return fetchIPv4LogEntry ($ip_bin);
1957		case 16: return fetchIPv6LogEntry ($ip_bin);
1958		default: throw new InvalidArgException ('ip_bin', $ip_bin, "Invalid binary IP");
1959	}
1960}
1961
1962function fetchIPv4LogEntry ($ip_bin)
1963{
1964	$result = usePreparedSelectBlade
1965	(
1966		"SELECT date, user, message FROM IPv4Log WHERE ip = ? ORDER BY date ASC",
1967		array (ip4_bin2db ($ip_bin))
1968	);
1969	return $result->fetchAll (PDO::FETCH_ASSOC);
1970}
1971
1972function fetchIPv6LogEntry ($ip_bin)
1973{
1974	$result = usePreparedSelectBlade
1975	(
1976		"SELECT date, user, message FROM IPv6Log WHERE ip = ? ORDER BY date ASC",
1977		array ($ip_bin)
1978	);
1979	return $result->fetchAll (PDO::FETCH_ASSOC);
1980}
1981
1982// wrapper around getObjectIPv4AllocationList and getObjectIPv6AllocationList
1983function getObjectIPAllocationList ($object_id)
1984{
1985	return
1986		getObjectIPv4AllocationList ($object_id) +
1987		getObjectIPv6AllocationList ($object_id);
1988}
1989
1990// Returns all IPv4 addresses allocated to object, but does not attach detailed info about address
1991// Used instead of getObjectIPv4Allocations if you need perfomance but 'addrinfo' value
1992function getObjectIPv4AllocationList ($object_id)
1993{
1994	$ret = array();
1995	$result = usePreparedSelectBlade
1996	(
1997		'SELECT name AS osif, type, ip FROM IPv4Allocation ' .
1998		'WHERE object_id = ?',
1999		array ($object_id)
2000	);
2001	while ($row = $result->fetch (PDO::FETCH_ASSOC))
2002		$ret[ip4_int2bin ($row['ip'])] = array ('osif' => $row['osif'], 'type' => $row['type']);
2003	return $ret;
2004}
2005
2006// Returns all IPv6 addresses allocated to object, but does not attach detailed info about address
2007// Used instead of getObjectIPv6Allocations if you need perfomance but 'addrinfo' value
2008function getObjectIPv6AllocationList ($object_id)
2009{
2010	$ret = array();
2011	$result = usePreparedSelectBlade
2012	(
2013		'SELECT name AS osif, type, ip AS ip FROM IPv6Allocation ' .
2014		'WHERE object_id = ?',
2015		array ($object_id)
2016	);
2017	while ($row = $result->fetch (PDO::FETCH_ASSOC))
2018		$ret[$row['ip']] = array ('osif' => $row['osif'], 'type' => $row['type']);
2019	return $ret;
2020}
2021
2022// Return all IP addresses allocated to the object sorted by allocation name.
2023// Attach detailed info about address to each alocation records.
2024// Index result by binary ip
2025function getObjectIPAllocations ($object_id)
2026{
2027	return amplifyAllocationList (getObjectIPAllocationList ($object_id));
2028}
2029function getObjectIPv4Allocations ($object_id)
2030{
2031	return amplifyAllocationList (getObjectIPv4AllocationList ($object_id));
2032}
2033function getObjectIPv6Allocations ($object_id)
2034{
2035	return amplifyAllocationList (getObjectIPv6AllocationList ($object_id));
2036}
2037
2038function amplifyAllocationList ($alloc_list)
2039{
2040	$ret = array();
2041	$sorted = array();
2042	foreach ($alloc_list as $ip_bin => $alloc)
2043		$sorted[$alloc['osif']][$ip_bin] = $alloc;
2044	foreach (sortPortList ($sorted) as $osif => $subarray)
2045		foreach ($subarray as $ip_bin => $alloc)
2046		{
2047			$alloc['addrinfo'] = getIPAddress ($ip_bin);
2048			$ret[$ip_bin] = $alloc;
2049		}
2050	return $ret;
2051}
2052
2053function scanIPNet ($net_info, $filter_flags = IPSCAN_ANY)
2054{
2055	$pairlist = array(
2056		array('first' => $net_info['ip_bin'], 'last' => ip_last ($net_info))
2057	);
2058	return scanIPSpace ($pairlist, $filter_flags);
2059}
2060
2061function scanIPSpace ($pairlist, $filter_flags = IPSCAN_ANY)
2062{
2063	$v4_pairs = array();
2064	$v6_pairs = array();
2065	foreach ($pairlist as $pair)
2066	{
2067		if (strlen ($pair['first']) == 4)
2068			$v4_pairs[] = $pair;
2069		elseif (strlen ($pair['first']) == 16)
2070			$v6_pairs[] = $pair;
2071	}
2072	return
2073		scanIPv4Space ($v4_pairs, $filter_flags) +
2074		scanIPv6Space ($v6_pairs, $filter_flags);
2075}
2076
2077// Check the range requested for meaningful IPv4 records, build them
2078// into a list and return. Return an empty list if nothing matched.
2079// Both arguments are expected in 4-byte binary string form. The resulting list
2080// is keyed by 4-byte binary IPs, items aren't sorted.
2081// LATER: accept a list of pairs and build WHERE sub-expression accordingly
2082function scanIPv4Space ($pairlist, $filter_flags = IPSCAN_ANY)
2083{
2084	$ret = array();
2085	if (!count ($pairlist)) // this is normal for a network completely divided into smaller parts
2086		return $ret;
2087	$pairlist = reduceIPPairList ($pairlist);
2088	// FIXME: this is a copy-and-paste prototype
2089	$or = '';
2090	$whereexpr1 = '(';
2091	$whereexpr2 = '(';
2092	$whereexpr3a = '(';
2093	$whereexpr3b = '(';
2094	$whereexpr4 = '(';
2095	$whereexpr5a = '(';
2096	$whereexpr5b = '(';
2097	$whereexpr6a = '(';
2098	$whereexpr6b = '(';
2099	$qparams = array();
2100	$qparams_bin = array();
2101	foreach ($pairlist as $tmp)
2102	{
2103		$whereexpr1 .= $or . "ip between ? and ?";
2104		$whereexpr2 .= $or . "ip between ? and ?";
2105		$whereexpr3a .= $or . "vip between ? and ?";
2106		$whereexpr3b .= $or . "vip between ? and ?";
2107		$whereexpr4 .= $or . "rsip between ? and ?";
2108		$whereexpr5a .= $or . "remoteip between ? and ?";
2109		$whereexpr5b .= $or . "localip between ? and ?";
2110		$whereexpr6a .= $or . "ip between ? and ?";
2111		$whereexpr6b .= $or . "l.ip between ? and ?";
2112		$or = ' or ';
2113		$qparams[] = ip4_bin2db ($tmp['first']);
2114		$qparams[] = ip4_bin2db ($tmp['last']);
2115		$qparams_bin[] = $tmp['first'];
2116		$qparams_bin[] = $tmp['last'];
2117	}
2118	$whereexpr1 .= ')';
2119	$whereexpr2 .= ')';
2120	$whereexpr3a .= ')';
2121	$whereexpr3b .= ')';
2122	$whereexpr4 .= ')';
2123	$whereexpr5a .= ')';
2124	$whereexpr5b .= ')';
2125	$whereexpr6a .= ')';
2126	$whereexpr6b .= ')';
2127
2128	// 1. collect labels and reservations
2129	if ($filter_flags & IPSCAN_DO_ADDR)
2130	{
2131	$query = "select ip, name, comment, reserved from IPv4Address ".
2132		"where ${whereexpr1} and (reserved = 'yes' or name != '' or comment != '')";
2133	$result = usePreparedSelectBlade ($query, $qparams);
2134	while ($row = $result->fetch (PDO::FETCH_ASSOC))
2135	{
2136		$ip_bin = ip4_int2bin ($row['ip']);
2137		if (!isset ($ret[$ip_bin]))
2138			$ret[$ip_bin] = constructIPAddress ($ip_bin);
2139		$ret[$ip_bin]['name'] = $row['name'];
2140		$ret[$ip_bin]['comment'] = $row['comment'];
2141		$ret[$ip_bin]['reserved'] = $row['reserved'];
2142	}
2143	unset ($result);
2144	}
2145
2146	// 2. check for allocations
2147	if ($filter_flags & IPSCAN_DO_ALLOCS)
2148	{
2149	if ($filter_flags & IPSCAN_RTR_ONLY)
2150		$whereexpr2 .= " AND ( ia.type = 'router' OR ia.type = 'sharedrouter')";
2151	$query =
2152		"select ia.ip, ia.object_id, ia.name, ia.type, Object.name as object_name " .
2153		"from IPv4Allocation AS ia INNER JOIN Object ON ia.object_id = Object.id where ${whereexpr2} order by ia.type";
2154	$result = usePreparedSelectBlade ($query, $qparams);
2155	while ($row = $result->fetch (PDO::FETCH_ASSOC))
2156	{
2157		$ip_bin = ip4_int2bin ($row['ip']);
2158		if (!isset ($ret[$ip_bin]))
2159			$ret[$ip_bin] = constructIPAddress ($ip_bin);
2160		$ret[$ip_bin]['allocs'][] = array
2161		(
2162			'type' => $row['type'],
2163			'name' => $row['name'],
2164			'object_id' => $row['object_id'],
2165			'object_name' => $row['object_name'],
2166		);
2167	}
2168	unset ($result);
2169	}
2170
2171	// 3a. look for virtual services
2172	if ($filter_flags & IPSCAN_DO_VS)
2173	{
2174	$query = "select id, vip from IPv4VS where ${whereexpr3a}";
2175	$result = usePreparedSelectBlade ($query, $qparams_bin);
2176	$allRows = $result->fetchAll (PDO::FETCH_ASSOC);
2177	unset ($result);
2178	foreach ($allRows as $row)
2179	{
2180		$ip_bin = $row['vip'];
2181		if (!isset ($ret[$ip_bin]))
2182			$ret[$ip_bin] = constructIPAddress ($ip_bin);
2183		$ret[$ip_bin]['vslist'][] = $row['id'];
2184	}
2185
2186	// 3b. look for virtual service groups
2187	$query = "select vs_id, vip from VSIPs where ${whereexpr3b}";
2188	$result = usePreparedSelectBlade ($query, $qparams_bin);
2189	$allRows = $result->fetchAll (PDO::FETCH_ASSOC);
2190	unset ($result);
2191	foreach ($allRows as $row)
2192	{
2193		$ip_bin = $row['vip'];
2194		if (!isset ($ret[$ip_bin]))
2195			$ret[$ip_bin] = constructIPAddress ($ip_bin);
2196		$ret[$ip_bin]['vsglist'][] = $row['vs_id'];
2197	}
2198	}
2199
2200	// 4. don't forget about real servers along with pools
2201	if ($filter_flags & IPSCAN_DO_RS)
2202	{
2203	$query = "select rsip, rspool_id from IPv4RS where ${whereexpr4}";
2204	$result = usePreparedSelectBlade ($query, $qparams_bin);
2205	while ($row = $result->fetch (PDO::FETCH_ASSOC))
2206	{
2207		$ip_bin = $row['rsip'];
2208		if (!isset ($ret[$ip_bin]))
2209			$ret[$ip_bin] = constructIPAddress ($ip_bin);
2210		$ret[$ip_bin]['rsplist'][] = $row['rspool_id'];
2211	}
2212	unset ($result);
2213	}
2214
2215	// 5. add NAT rules, remote ip
2216	if ($filter_flags & IPSCAN_DO_NAT)
2217	{
2218	$query =
2219		"select " .
2220		"proto, " .
2221		"localip, " .
2222		"localport, " .
2223		"remoteip, " .
2224		"remoteport, " .
2225		"description " .
2226		"from IPv4NAT " .
2227		"where ${whereexpr5a} " .
2228		"order by localip, localport, remoteip, remoteport, proto";
2229	$result = usePreparedSelectBlade ($query, $qparams);
2230	while ($row = $result->fetch (PDO::FETCH_ASSOC))
2231	{
2232		$ip_bin_local = ip4_int2bin ($row['localip']);
2233		$ip_bin_remote = ip4_int2bin ($row['remoteip']);
2234		$row['localip_bin'] = $ip_bin_local;
2235		$row['remoteip_bin'] = $ip_bin_remote;
2236		$row['localip'] = ip_format ($ip_bin_local);
2237		$row['remoteip'] = ip_format ($ip_bin_remote);
2238		if (!isset ($ret[$ip_bin_remote]))
2239			$ret[$ip_bin_remote] = constructIPAddress ($ip_bin_remote);
2240		$ret[$ip_bin_remote]['inpf'][] = $row;
2241	}
2242	unset ($result);
2243	// 5. add NAT rules, local ip
2244	$query =
2245		"select " .
2246		"proto, " .
2247		"localip, " .
2248		"localport, " .
2249		"remoteip, " .
2250		"remoteport, " .
2251		"description " .
2252		"from IPv4NAT " .
2253		"where ${whereexpr5b} " .
2254		"order by localip, localport, remoteip, remoteport, proto";
2255	$result = usePreparedSelectBlade ($query, $qparams);
2256	while ($row = $result->fetch (PDO::FETCH_ASSOC))
2257	{
2258		$ip_bin_local = ip4_int2bin ($row['localip']);
2259		$ip_bin_remote = ip4_int2bin ($row['remoteip']);
2260		$row['localip_bin'] = $ip_bin_local;
2261		$row['remoteip_bin'] = $ip_bin_remote;
2262		$row['localip'] = ip_format ($ip_bin_local);
2263		$row['remoteip'] = ip_format ($ip_bin_remote);
2264		if (!isset ($ret[$ip_bin_local]))
2265			$ret[$ip_bin_local] = constructIPAddress ($ip_bin_local);
2266		$ret[$ip_bin_local]['outpf'][] = $row;
2267	}
2268	unset ($result);
2269	}
2270
2271	// 6. collect last log message
2272	if ($filter_flags & IPSCAN_DO_LOG)
2273	{
2274	$query = "select l.ip, l.user, UNIX_TIMESTAMP(l.date) AS time from IPv4Log l INNER JOIN " .
2275		" (SELECT MAX(id) as id FROM IPv4Log WHERE ${whereexpr6a} GROUP BY ip) v USING (id) WHERE ${whereexpr6b}";
2276	$result = usePreparedSelectBlade ($query, array_merge ($qparams, $qparams));
2277	while ($row = $result->fetch (PDO::FETCH_ASSOC))
2278	{
2279		$ip_bin = ip4_int2bin ($row['ip']);
2280		if (isset ($ret[$ip_bin]))
2281			$ret[$ip_bin]['last_log'] = array
2282			(
2283				'user' => $row['user'],
2284				'time' => $row['time'],
2285			);
2286	}
2287	unset ($result);
2288	}
2289
2290	$override = callHook ('scanIPSpace_hook', $ret, $pairlist, $filter_flags);
2291	if (isset ($override))
2292		$ret = $override;
2293
2294	return $ret;
2295}
2296
2297// Check the range requested for meaningful IPv6 records, build them
2298// into a list and return. Return an empty list if nothing matched.
2299// Both arguments are expected as 16-byte binary IPs. The resulting list
2300// is keyed by 16-byte bynary IPs, items aren't sorted.
2301function scanIPv6Space ($pairlist, $filter_flags = IPSCAN_ANY)
2302{
2303	$ret = array();
2304	if (!count ($pairlist)) // this is normal for a network completely divided into smaller parts
2305		return $ret;
2306	$pairlist = reduceIPPairList ($pairlist);
2307
2308	$or = '';
2309	$whereexpr1 = '(';
2310	$whereexpr2 = '(';
2311	$whereexpr3a = '(';
2312	$whereexpr3b = '(';
2313	$whereexpr4 = '(';
2314	$whereexpr6a = '(';
2315	$whereexpr6b = '(';
2316	$qparams = array();
2317	foreach ($pairlist as $tmp)
2318	{
2319		$whereexpr1 .= $or . "ip between ? and ?";
2320		$whereexpr2 .= $or . "ip between ? and ?";
2321		$whereexpr3a .= $or . "vip between ? and ?";
2322		$whereexpr3b .= $or . "vip between ? and ?";
2323		$whereexpr4 .= $or . "rsip between ? and ?";
2324		$whereexpr6a .= $or . "ip between ? and ?";
2325		$whereexpr6b .= $or . "l.ip between ? and ?";
2326		$or = ' or ';
2327		$qparams[] = $tmp['first'];
2328		$qparams[] = $tmp['last'];
2329	}
2330	$whereexpr1 .= ')';
2331	$whereexpr2 .= ')';
2332	$whereexpr3a .= ')';
2333	$whereexpr3b .= ')';
2334	$whereexpr4 .= ')';
2335	$whereexpr6a .= ')';
2336	$whereexpr6b .= ')';
2337
2338	// 1. collect labels and reservations
2339	if ($filter_flags & IPSCAN_DO_ADDR)
2340	{
2341	$query = "select ip, name, comment, reserved from IPv6Address ".
2342		"where ${whereexpr1} and (reserved = 'yes' or name != '' or comment != '')";
2343	$result = usePreparedSelectBlade ($query, $qparams);
2344	while ($row = $result->fetch (PDO::FETCH_ASSOC))
2345	{
2346		$ip_bin = $row['ip'];
2347		if (!isset ($ret[$ip_bin]))
2348			$ret[$ip_bin] = constructIPAddress ($ip_bin);
2349		$ret[$ip_bin]['name'] = $row['name'];
2350		$ret[$ip_bin]['comment'] = $row['comment'];
2351		$ret[$ip_bin]['reserved'] = $row['reserved'];
2352	}
2353	unset ($result);
2354	}
2355
2356	// 2. check for allocations
2357	if ($filter_flags & IPSCAN_DO_ALLOCS)
2358	{
2359	if ($filter_flags & IPSCAN_RTR_ONLY)
2360		$whereexpr2 .= " AND (ia.type = 'router' OR ia.type = 'sharedrouter')";
2361	$query =
2362		"select ia.ip, ia.object_id, ia.name, ia.type, Object.name as object_name " .
2363		"from IPv6Allocation AS ia INNER JOIN Object ON ia.object_id = Object.id where ${whereexpr2} order by ia.type";
2364	$result = usePreparedSelectBlade ($query, $qparams);
2365	while ($row = $result->fetch (PDO::FETCH_ASSOC))
2366	{
2367		$ip_bin = $row['ip'];
2368		if (!isset ($ret[$ip_bin]))
2369			$ret[$ip_bin] = constructIPAddress ($ip_bin);
2370		$ret[$ip_bin]['allocs'][] = array
2371		(
2372			'type' => $row['type'],
2373			'name' => $row['name'],
2374			'object_id' => $row['object_id'],
2375			'object_name' => $row['object_name'],
2376		);
2377	}
2378	unset ($result);
2379	}
2380
2381	// 3a. look for virtual services
2382	if ($filter_flags & IPSCAN_DO_VS)
2383	{
2384	$query = "select id, vip from IPv4VS where ${whereexpr3a}";
2385	$result = usePreparedSelectBlade ($query, $qparams);
2386	$allRows = $result->fetchAll (PDO::FETCH_ASSOC);
2387	unset ($result);
2388	foreach ($allRows as $row)
2389	{
2390		$ip_bin = $row['vip'];
2391		if (!isset ($ret[$ip_bin]))
2392			$ret[$ip_bin] = constructIPAddress ($ip_bin);
2393		$ret[$ip_bin]['vslist'][] = $row['id'];
2394	}
2395
2396	// 3b. look for virtual service groups
2397	$query = "select vs_id, vip from VSIPs where ${whereexpr3b}";
2398	$result = usePreparedSelectBlade ($query, $qparams);
2399	$allRows = $result->fetchAll (PDO::FETCH_ASSOC);
2400	unset ($result);
2401	foreach ($allRows as $row)
2402	{
2403		$ip_bin = $row['vip'];
2404		if (!isset ($ret[$ip_bin]))
2405			$ret[$ip_bin] = constructIPAddress ($ip_bin);
2406		$ret[$ip_bin]['vsglist'][] = $row['vs_id'];
2407	}
2408	}
2409
2410	// 4. don't forget about real servers along with pools
2411	if ($filter_flags & IPSCAN_DO_RS)
2412	{
2413	$query = "select rsip, rspool_id from IPv4RS where ${whereexpr4}";
2414	$result = usePreparedSelectBlade ($query, $qparams);
2415	while ($row = $result->fetch (PDO::FETCH_ASSOC))
2416	{
2417		$ip_bin = $row['rsip'];
2418		if (!isset ($ret[$ip_bin]))
2419			$ret[$ip_bin] = constructIPAddress ($ip_bin);
2420		$ret[$ip_bin]['rsplist'][] = $row['rspool_id'];
2421	}
2422	unset ($result);
2423	}
2424
2425	// 6. collect last log message
2426	if ($filter_flags & IPSCAN_DO_LOG)
2427	{
2428	$query = "select l.ip, l.user, UNIX_TIMESTAMP(l.date) AS time from IPv6Log l INNER JOIN " .
2429		" (SELECT MAX(id) as id FROM IPv6Log WHERE ${whereexpr6a} GROUP BY ip) v USING (id) WHERE ${whereexpr6b}";
2430	$result = usePreparedSelectBlade ($query, array_merge ($qparams, $qparams));
2431	while ($row = $result->fetch (PDO::FETCH_ASSOC))
2432	{
2433		$ip_bin = $row['ip'];
2434		if (isset ($ret[$ip_bin]))
2435			$ret[$ip_bin]['last_log'] = array
2436			(
2437				'user' => $row['user'],
2438				'time' => $row['time'],
2439			);
2440	}
2441	unset ($result);
2442	}
2443
2444	$override = callHook ('scanIPSpace_hook', $ret, $pairlist, $filter_flags);
2445	if (isset ($override))
2446		$ret = $override;
2447
2448	return $ret;
2449}
2450
2451function bindIPToObject ($ip_bin, $object_id = 0, $name = '', $type = '')
2452{
2453	switch (strlen ($ip_bin))
2454	{
2455		case 4:
2456			$db_ip = ip4_bin2db ($ip_bin);
2457			$table = 'IPv4Allocation';
2458			$table2 = 'IPv4Address';
2459			break;
2460		case 16:
2461			$db_ip = $ip_bin;
2462			$table = 'IPv6Allocation';
2463			$table2 = 'IPv6Address';
2464			break;
2465		default: throw new InvalidArgException ('ip_bin', $ip_bin, "Invalid binary IP");
2466	}
2467
2468	// release IP reservation and/or comment if configured
2469	$release = getConfigVar ('IPV4_AUTO_RELEASE');
2470	if ($release >= 2)
2471		usePreparedExecuteBlade ("DELETE FROM $table2 WHERE ip = ?", array ($db_ip));
2472	elseif ($release >= 1)
2473		usePreparedExecuteBlade ("UPDATE $table2 SET reserved = 'no' WHERE ip = ?", array ($db_ip));
2474
2475	usePreparedInsertBlade
2476	(
2477		$table,
2478		array ('ip' => $db_ip, 'object_id' => $object_id, 'name' => $name, 'type' => $type)
2479	);
2480	// store history line
2481	$cell = spotEntity ('object', $object_id);
2482	setDisplayedName ($cell);
2483	addIPLogEntry ($ip_bin, "Binded with ${cell['dname']}, ifname=$name");
2484}
2485
2486function bindIPv4ToObject ($ip_bin, $object_id = 0, $name = '', $type = '')
2487{
2488	if (strlen ($ip_bin) != 4)
2489		throw new InvalidArgException ('ip_bin', $ip_bin, "Invalid binary IP");
2490	return bindIPToObject ($ip_bin, $object_id, $name, $type);
2491}
2492
2493function bindIPv6ToObject ($ip_bin, $object_id = 0, $name = '', $type = '')
2494{
2495	if (strlen ($ip_bin) != 16)
2496		throw new InvalidArgException ('ip_bin', $ip_bin, "Invalid binary IP");
2497	return bindIPToObject ($ip_bin, $object_id, $name, $type);
2498}
2499
2500// Universal v4/v6 wrapper around getIPv4AddressNetworkId and getIPv6AddressNetworkId.
2501// Return the id of the smallest IP network containing the given IP address
2502// or NULL, if nothing was found. When finding the covering network for
2503// another network, it is important to filter out matched records with longer
2504// masks (they aren't going to be the right pick).
2505function getIPAddressNetworkId ($ip_bin, $masklen = NULL)
2506{
2507	switch (strlen ($ip_bin))
2508	{
2509		case 4:  return getIPv4AddressNetworkId ($ip_bin, isset ($masklen) ? $masklen : 32);
2510		case 16: return getIPv6AddressNetworkId ($ip_bin, isset ($masklen) ? $masklen : 128);
2511		default: throw new InvalidArgException ('ip_bin', $ip_bin, "Invalid binary IP");
2512	}
2513}
2514
2515// Returns ipv4net or ipv6net entity, or NULL if no spanning network found.
2516// Throws an exception if $ip_bin is not valid binary address;
2517function spotNetworkByIP ($ip_bin, $masklen = NULL)
2518{
2519	$net_id = getIPAddressNetworkId ($ip_bin, $masklen);
2520	if (! $net_id)
2521		return NULL;
2522	switch (strlen ($ip_bin))
2523	{
2524		case 4:  return spotEntity ('ipv4net', $net_id);
2525		case 16: return spotEntity ('ipv6net', $net_id);
2526		default: throw new InvalidArgException ('ip_bin', $ip_bin, "Invalid binary IP");
2527	}
2528}
2529
2530// Return the id of the smallest IPv4 network containing the given IPv4 address
2531// or NULL, if nothing was found. When finding the covering network for
2532// another network, it is important to filter out matched records with longer
2533// masks (they aren't going to be the right pick).
2534function getIPv4AddressNetworkId ($ip_bin, $masklen = 32)
2535{
2536	$row = callHook ('fetchIPv4AddressNetworkRow', $ip_bin, $masklen);
2537	return $row === NULL ? NULL : $row['id'];
2538}
2539
2540function fetchIPAddressNetworkRow ($ip_bin, $masklen = NULL)
2541{
2542	switch (strlen ($ip_bin))
2543	{
2544	case 4:
2545		return callHook ('fetchIPv4AddressNetworkRow', $ip_bin, isset ($masklen) ? $masklen : 32);
2546	case 16:
2547		return callHook ('fetchIPv6AddressNetworkRow', $ip_bin, isset ($masklen) ? $masklen : 128);
2548	default:
2549		throw new InvalidArgException ('ip_bin', $ip_bin, "Invalid binary address");
2550	}
2551}
2552
2553function fetchIPv4AddressNetworkRow ($ip_bin, $masklen = 32)
2554{
2555	$ip_db = ip4_bin2db ($ip_bin);
2556	$result = usePreparedSelectBlade
2557	(
2558		'SELECT id, ip, mask, name, comment FROM IPv4Network ' .
2559		'WHERE ? & (4294967295 >> (32 - mask)) << (32 - mask) = ip AND ip <= ? AND mask < ? ' .
2560		'ORDER BY mask DESC LIMIT 1',
2561		array ($ip_db, $ip_db, $masklen)
2562	);
2563	return nullIfFalse ($result->fetch (PDO::FETCH_ASSOC));
2564}
2565
2566// Return the id of the smallest IPv6 network containing the given IPv6 address
2567// ($ip is an instance of IPv4Address class) or NULL, if nothing was found.
2568function getIPv6AddressNetworkId ($ip_bin, $masklen = 128)
2569{
2570	$row = callHook ('fetchIPv6AddressNetworkRow', $ip_bin, $masklen);
2571	return $row === NULL ? NULL : $row['id'];
2572}
2573
2574function fetchIPv6AddressNetworkRow ($ip_bin, $masklen = 128)
2575{
2576	$result = usePreparedSelectBlade
2577	(
2578		'SELECT id, ip, mask, last_ip, name, comment FROM IPv6Network ' .
2579		'WHERE ip <= ? AND last_ip >= ? AND mask < ? ' .
2580		'ORDER BY mask DESC LIMIT 1',
2581		array ($ip_bin, $ip_bin, $masklen)
2582	);
2583	return nullIfFalse ($result->fetch (PDO::FETCH_ASSOC));
2584}
2585
2586// This function is actually used not only to update, but also to create records,
2587// that's why ON DUPLICATE KEY UPDATE was replaced by DELETE-INSERT pair
2588// (MySQL 4.0 workaround).
2589function updateAddress ($ip_bin, $name = '', $reserved = 'no', $comment = '')
2590{
2591	switch (strlen ($ip_bin))
2592	{
2593		case 4:
2594			$table = 'IPv4Address';
2595			$db_ip = ip4_bin2db ($ip_bin);
2596			break;
2597		case 16:
2598			$table = 'IPv6Address';
2599			$db_ip = $ip_bin;
2600			break;
2601		default: throw new InvalidArgException ('ip_bin', $ip_bin, "Invalid binary IP");
2602	}
2603
2604	// compute update log message
2605	$result = usePreparedSelectBlade ("SELECT name, comment, reserved FROM $table WHERE ip = ?", array ($db_ip));
2606	$old_name = '';
2607	$old_comment = '';
2608	if ($row = $result->fetch (PDO::FETCH_ASSOC))
2609	{
2610		$old_name = $row['name'];
2611		$old_comment = $row['comment'];
2612	}
2613
2614	// If the 'comment' argument was specified when this function was called, use it.
2615	// If not, retain the old value.
2616	$comment = (func_num_args () == 4 ) ? $comment : $old_comment;
2617	$new_row = array ('name' => $name, 'comment' => $comment, 'reserved' => $reserved);
2618	$new_row_empty = $name == '' && $comment == '' && $reserved == 'no';
2619
2620	unset ($result);
2621	$messages = array ();
2622	if ($name != $old_name)
2623	{
2624		if ($name == '')
2625			$messages[] = "name '$old_name' removed";
2626		elseif ($old_name == '')
2627			$messages[] = "name set to '$name'";
2628		else
2629			$messages[] = "name changed from '$old_name' to '$name'";
2630	}
2631	if ($comment != $old_comment)
2632	{
2633		if ($comment == '')
2634			$messages[] = "comment '$old_comment' removed";
2635		elseif ($old_name == '')
2636			$messages[] = "comment set to '$comment'";
2637		else
2638			$messages[] = "comment changed from '$old_comment' to '$comment'";
2639	}
2640
2641	if ($row && ! $new_row_empty && $row == $new_row)
2642		return;
2643	if ($row)
2644		usePreparedDeleteBlade ($table, array ('ip' => $db_ip));
2645	// INSERT may appear not necessary.
2646	if (! $new_row_empty)
2647		usePreparedInsertBlade
2648		(
2649			$table,
2650			array ('name' => $name, 'comment' => $comment, 'reserved' => $reserved, 'ip' => $db_ip)
2651		);
2652	// store history line
2653	if ($messages)
2654		addIPLogEntry ($ip_bin, ucfirst (implode (', ', $messages)));
2655}
2656
2657function updateV4Address ($ip_bin, $name = '', $reserved = 'no', $comment = '')
2658{
2659	if (strlen ($ip_bin) != 4)
2660		throw new InvalidArgException ('ip_bin', $ip_bin, "Invalid binary IP");
2661	return updateAddress ($ip_bin, $name, $reserved, $comment);
2662}
2663
2664function updateV6Address ($ip_bin, $name = '', $reserved = 'no', $comment = '')
2665{
2666	if (strlen ($ip_bin) != 16)
2667		throw new InvalidArgException ('ip_bin', $ip_bin, "Invalid binary IP");
2668	return updateAddress ($ip_bin, $name, $reserved, $comment);
2669}
2670
2671function updateIPBond ($ip_bin, $object_id=0, $name='', $type='')
2672{
2673	switch (strlen ($ip_bin))
2674	{
2675		case 4:  return updateIPv4Bond ($ip_bin, $object_id, $name, $type);
2676		case 16: return updateIPv6Bond ($ip_bin, $object_id, $name, $type);
2677		default: throw new InvalidArgException ('ip_bin', $ip_bin, "Invalid binary IP");
2678	}
2679}
2680
2681function updateIPv4Bond ($ip_bin, $object_id=0, $name='', $type='')
2682{
2683	usePreparedUpdateBlade
2684	(
2685		'IPv4Allocation',
2686		array
2687		(
2688			'name' => $name,
2689			'type' => $type,
2690		),
2691		array
2692		(
2693			'ip' => ip4_bin2db ($ip_bin),
2694			'object_id' => $object_id,
2695		)
2696	);
2697}
2698
2699function updateIPv6Bond ($ip_bin, $object_id=0, $name='', $type='')
2700{
2701	usePreparedUpdateBlade
2702	(
2703		'IPv6Allocation',
2704		array
2705		(
2706			'name' => $name,
2707			'type' => $type,
2708		),
2709		array
2710		(
2711			'ip' => $ip_bin,
2712			'object_id' => $object_id,
2713		)
2714	);
2715}
2716
2717function unbindIPFromObject ($ip_bin, $object_id)
2718{
2719	switch (strlen ($ip_bin))
2720	{
2721		case 4:
2722			$table = 'IPv4Allocation';
2723			$db_ip = ip4_bin2db ($ip_bin);
2724			break;
2725		case 16:
2726			$table = 'IPv6Allocation';
2727			$db_ip = $ip_bin;
2728			break;
2729		default: throw new InvalidArgException ('ip_bin', $ip_bin, "Invalid binary IP");
2730	}
2731
2732	$n_deleted = usePreparedDeleteBlade
2733	(
2734		$table,
2735		array ('ip' => $db_ip, 'object_id' => $object_id)
2736	);
2737	if ($n_deleted)
2738	{
2739		// store history line
2740		$cell = spotEntity ('object', $object_id);
2741		setDisplayedName ($cell);
2742		addIPLogEntry ($ip_bin, "Removed from ${cell['dname']}");
2743	}
2744}
2745
2746function unbindIPv4FromObject ($ip_bin, $object_id)
2747{
2748	if (strlen ($ip_bin) != 4)
2749		throw new InvalidArgException ('ip_bin', $ip_bin, "Invalid binary IP");
2750	return unbindIPFromObject ($ip_bin, $object_id);
2751}
2752
2753function unbindIPv6FromObject ($ip_bin, $object_id)
2754{
2755	if (strlen ($ip_bin) != 16)
2756		throw new InvalidArgException ('ip_bin', $ip_bin, "Invalid binary IP");
2757	return unbindIPFromObject ($ip_bin, $object_id);
2758}
2759
2760function getIPv4PrefixSearchResult ($terms)
2761{
2762	$ret = array();
2763	foreach (array ('name', 'comment') as $column)
2764	{
2765		$tmp = getSearchResultByField
2766		(
2767			'IPv4Network',
2768			array ('id'),
2769			$column,
2770			$terms,
2771			'ip'
2772		);
2773		foreach ($tmp as $row)
2774			$ret[$row['id']] = spotEntity ('ipv4net', $row['id']);
2775	}
2776	return $ret;
2777}
2778
2779function getIPv6PrefixSearchResult ($terms)
2780{
2781	$ret = array();
2782	foreach (array ('name', 'comment') as $column)
2783	{
2784		$tmp = getSearchResultByField
2785		(
2786			'IPv6Network',
2787			array ('id'),
2788			$column,
2789			$terms,
2790			'ip'
2791		);
2792		foreach ($tmp as $row)
2793			$ret[$row['id']] = spotEntity ('ipv6net', $row['id']);
2794	}
2795	return $ret;
2796}
2797
2798function getIPv4AddressSearchResult ($terms)
2799{
2800	$query = "select ip, name, comment from IPv4Address where ";
2801	$or = '';
2802	$qparams = array();
2803	foreach (explode (' ', $terms) as $term)
2804	{
2805		$query .= $or . "name like ? or comment like ?";
2806		$or = ' or ';
2807		$qparams[] = "%${term}%";
2808		$qparams[] = "%${term}%";
2809	}
2810	$result = usePreparedSelectBlade ($query, $qparams);
2811	$ret = array();
2812	while ($row = $result->fetch (PDO::FETCH_ASSOC))
2813	{
2814		$ip_bin = ip4_int2bin ($row['ip']);
2815		$row['ip'] = $ip_bin;
2816		$ret[$ip_bin] = $row;
2817	}
2818	return $ret;
2819}
2820
2821function getIPv6AddressSearchResult ($terms)
2822{
2823	$query = "select ip, name, comment from IPv6Address where ";
2824	$or = '';
2825	$qparams = array();
2826	foreach (explode (' ', $terms) as $term)
2827	{
2828		$query .= $or . "name like ? or comment like ?";
2829		$or = ' or ';
2830		$qparams[] = "%${term}%";
2831		$qparams[] = "%${term}%";
2832	}
2833	$result = usePreparedSelectBlade ($query, $qparams);
2834	return reindexById ($result->fetchAll (PDO::FETCH_ASSOC), 'ip');
2835}
2836
2837function getIPv4RSPoolSearchResult ($terms)
2838{
2839	$byname = getSearchResultByField
2840	(
2841		'IPv4RSPool',
2842		array ('id'),
2843		'name',
2844		$terms,
2845		'name'
2846	);
2847	$ret = array();
2848	foreach ($byname as $row)
2849		$ret[$row['id']] = spotEntity ('ipv4rspool', $row['id']);
2850	return $ret;
2851}
2852
2853function getIPv4VServiceSearchResult ($terms)
2854{
2855	$byname = getSearchResultByField
2856	(
2857		'IPv4VS',
2858		array ('id'),
2859		'name',
2860		$terms,
2861		'vip'
2862	);
2863	$ret = array();
2864	foreach ($byname as $row)
2865		$ret[$row['id']] = spotEntity ('ipv4vs', $row['id']);
2866	return $ret;
2867}
2868
2869function getVServiceSearchResult ($terms)
2870{
2871	$byname = getSearchResultByField
2872	(
2873		'VS',
2874		array ('id'),
2875		'name',
2876		$terms,
2877		'name'
2878	);
2879	$ret = array();
2880	foreach ($byname as $row)
2881		$ret[$row['id']] = spotEntity ('ipvs', $row['id']);
2882	return $ret;
2883}
2884
2885function getAccountSearchResult ($terms)
2886{
2887	$byUsername = getSearchResultByField
2888	(
2889		'UserAccount',
2890		array ('user_id'),
2891		'user_name',
2892		$terms,
2893		'user_name'
2894	);
2895	$byRealname = getSearchResultByField
2896	(
2897		'UserAccount',
2898		array ('user_id'),
2899		'user_realname',
2900		$terms,
2901		'user_name'
2902	);
2903	// Merge it together, if duplicates persist, byUsername wins
2904	$ret = array();
2905	foreach (array ($byRealname, $byUsername) as $array)
2906		foreach ($array as $user)
2907			$ret[$user['user_id']] = spotEntity ('user', $user['user_id']);
2908	return $ret;
2909}
2910
2911function getFileSearchResult ($terms)
2912{
2913	$byName = getSearchResultByField
2914	(
2915		'File',
2916		array ('id'),
2917		'name',
2918		$terms,
2919		'name'
2920	);
2921	$byComment = getSearchResultByField
2922	(
2923		'File',
2924		array ('id'),
2925		'comment',
2926		$terms,
2927		'name'
2928	);
2929	// Filter out dupes.
2930	foreach ($byName as $res1)
2931		foreach (array_keys ($byComment) as $key2)
2932			if ($res1['id'] == $byComment[$key2]['id'])
2933			{
2934				unset ($byComment[$key2]);
2935				continue 2;
2936			}
2937	$ret = array();
2938	foreach (array_merge ($byName, $byComment) as $row)
2939		$ret[$row['id']] = spotEntity ('file', $row['id']);
2940	return $ret;
2941}
2942
2943function getRackSearchResult ($terms)
2944{
2945	$byName = getSearchResultByField
2946	(
2947		'Rack',
2948		array ('id'),
2949		'name',
2950		$terms,
2951		'name'
2952	);
2953	$byComment = getSearchResultByField
2954	(
2955		'Rack',
2956		array ('id'),
2957		'comment',
2958		$terms,
2959		'name'
2960	);
2961	$byAssetNo = getSearchResultByField
2962	(
2963		'Rack',
2964		array ('id'),
2965		'asset_no',
2966		$terms,
2967		'name'
2968	);
2969	$bySticker = getStickerSearchResults ('Rack', $terms);
2970	// Filter out dupes.
2971	foreach ($byName as $res1)
2972	{
2973		foreach (array_keys ($byComment) as $key2)
2974			if ($res1['id'] == $byComment[$key2]['id'])
2975				unset ($byComment[$key2]);
2976		foreach (array_keys ($byAssetNo) as $key3)
2977			if ($res1['id'] == $byAssetNo[$key3]['id'])
2978				unset ($byAssetNo[$key3]);
2979		foreach (array_keys ($bySticker) as $key4)
2980			if ($res1['id'] == $bySticker[$key4]['id'])
2981				unset ($bySticker[$key4]);
2982	}
2983	$ret = array();
2984	foreach (array_merge ($byName, $byComment, $byAssetNo, $bySticker) as $row)
2985		$ret[$row['id']] = spotEntity ('rack', $row['id']);
2986	return $ret;
2987}
2988
2989function getRowSearchResult ($terms)
2990{
2991	$byName = getSearchResultByField
2992	(
2993		'`Row`',
2994		array ('id'),
2995		'name',
2996		$terms,
2997		'name'
2998	);
2999
3000	$ret = array();
3001	foreach ($byName as $row)
3002		$ret[$row['id']] = spotEntity ('row', $row['id']);
3003	return $ret;
3004}
3005
3006function getLocationSearchResult ($terms)
3007{
3008	$byName = getSearchResultByField
3009	(
3010		'Location',
3011		array ('id'),
3012		'name',
3013		$terms,
3014		'name'
3015	);
3016	$byComment = getSearchResultByField
3017	(
3018		'Location',
3019		array ('id'),
3020		'comment',
3021		$terms,
3022		'name'
3023	);
3024	$bySticker = getStickerSearchResults ('Location', $terms);
3025	// Filter out dupes.
3026	foreach ($byName as $res1)
3027	{
3028		foreach (array_keys ($byComment) as $key2)
3029			if ($res1['id'] == $byComment[$key2]['id'])
3030				unset ($byComment[$key2]);
3031		foreach (array_keys ($bySticker) as $key3)
3032			if ($res1['id'] == $bySticker[$key3]['id'])
3033				unset ($bySticker[$key3]);
3034	}
3035	$ret = array();
3036	foreach (array_merge ($byName, $byComment, $bySticker) as $location)
3037		$ret[$location['id']] = spotEntity ('location', $location['id']);
3038	return $ret;
3039}
3040
3041function getVLANSearchResult ($terms)
3042{
3043	$ret = array();
3044	$byDescr = getSearchResultByField
3045	(
3046		'VLANDescription',
3047		array ('domain_id', 'vlan_id'),
3048		'vlan_descr',
3049		$terms
3050	);
3051	foreach ($byDescr as $row)
3052	{
3053		$vlan_ck = $row['domain_id'] . '-' . $row['vlan_id'];
3054		$row['id'] = $vlan_ck;
3055		$ret[$vlan_ck] = $row;
3056	}
3057	return $ret;
3058}
3059
3060function getSearchResultByField ($tablename, $retcolumns, $scancolumn, $terms, $ordercolumn = '', $exactness = 0)
3061{
3062	$query = 'SELECT ' . implode (', ', $retcolumns) . " FROM ${tablename} WHERE ";
3063	$qparams = array();
3064	$pfx = '';
3065	$pterms = $exactness == 3 ? explode (' ', $terms) : parseSearchTerms ($terms);
3066	foreach ($pterms as $term)
3067	{
3068		switch ($exactness)
3069		{
3070		case 3:
3071			$query .= $pfx . "${scancolumn} REGEXP ?";
3072			$qparams[] = $term;
3073			break;
3074		case 2: // does this work as expected?
3075			$query .= $pfx . "BINARY ${scancolumn} = ?";
3076			$qparams[] = $term;
3077			break;
3078		case 1:
3079			$query .= $pfx . "${scancolumn} = ?";
3080			$qparams[] = $term;
3081			break;
3082		default:
3083			$query .= $pfx . "${scancolumn} LIKE ?";
3084			$qparams[] = "%${term}%";
3085			break;
3086		}
3087		$pfx = ' OR ';
3088	}
3089	if ($ordercolumn != '')
3090		$query .= " ORDER BY ${ordercolumn}";
3091	$result = usePreparedSelectBlade ($query, $qparams);
3092	return $result->fetchAll (PDO::FETCH_ASSOC);
3093}
3094
3095function getObjectSearchResults ($what)
3096{
3097	$ret = array();
3098	global $searchfunc;
3099	foreach ($searchfunc['object'] as $method => $func)
3100		foreach ($func ($what) as $objRecord)
3101		{
3102			$ret[$objRecord['id']]['id'] = $objRecord['id'];
3103			$ret[$objRecord['id']][$method] = $objRecord[$method];
3104		}
3105	foreach (getStickerSearchResults ('RackObject', $what) as $objRecord)
3106	{
3107		$ret[$objRecord['id']]['id'] = $objRecord['id'];
3108		$ret[$objRecord['id']]['by_sticker'] = $objRecord['by_sticker'];
3109	}
3110	return $ret;
3111}
3112
3113function getObjectAttrsSearchResults ($what)
3114{
3115	$ret = array();
3116	foreach (array ('name', 'label', 'asset_no', 'comment') as $column)
3117	{
3118		$tmp = getSearchResultByField
3119		(
3120			'RackObject',
3121			array ('id'),
3122			$column,
3123			$what,
3124			$column
3125		);
3126		foreach ($tmp as $row)
3127		{
3128			$ret[$row['id']]['id'] = $row['id'];
3129			$ret[$row['id']]['by_attr'][] = $column;
3130		}
3131	}
3132	return $ret;
3133}
3134
3135// Search stickers and return a list of pairs "object_id-attribute_id"
3136// that matched. A partilar object_id could be returned more than once, if it has
3137// multiple matching stickers. Search is only performed on "string" or "dict" attributes.
3138function getStickerSearchResults ($tablename, $what)
3139{
3140	$attr_types = array();
3141	$result = usePreparedSelectBlade
3142	(
3143		'SELECT AV.object_id, AV.attr_id FROM AttributeValue AV ' .
3144		"INNER JOIN ${tablename} O ON AV.object_id = O.id " .
3145		'INNER JOIN Attribute A ON AV.attr_id = A.id ' .
3146		'LEFT JOIN AttributeMap AM ON A.type = "dict" AND AV.object_tid = AM.objtype_id AND AV.attr_id = AM.attr_id ' .
3147		'LEFT JOIN Dictionary D ON AM.chapter_id = D.chapter_id AND AV.uint_value = D.dict_key ' .
3148		'WHERE string_value LIKE ? ' .
3149		'OR (A.type = "dict" AND dict_value LIKE ?) ORDER BY object_id',
3150		array ("%${what}%", "%${what}%")
3151	);
3152	$ret = array ();
3153	while ($row = $result->fetch (PDO::FETCH_ASSOC))
3154	{
3155		if (! array_key_exists ($row['attr_id'], $attr_types))
3156			$attr_types[$row['attr_id']] = getAttrType ($row['attr_id']);
3157		if (in_array ($attr_types[$row['attr_id']], array ('string', 'dict')))
3158		{
3159			$ret[$row['object_id']]['id'] = $row['object_id'];
3160			$ret[$row['object_id']]['by_sticker'][] = $row['attr_id'];
3161		}
3162	}
3163	return $ret;
3164}
3165
3166// search in port "reservation comment", "label" and "L2 address" columns
3167function getPortSearchResults ($what)
3168{
3169	$ret = array();
3170	$ports = getSearchResultByField
3171	(
3172		'Port',
3173		array ('object_id', 'id', 'reservation_comment'),
3174		'reservation_comment',
3175		$what,
3176		'object_id',
3177		0
3178	);
3179	foreach ($ports as $port)
3180	{
3181		$ret[$port['object_id']]['id'] = $port['object_id'];
3182		$ret[$port['object_id']]['by_port'][$port['id']] = $port['reservation_comment'];
3183	}
3184	$ports = getSearchResultByField
3185	(
3186		'Port',
3187		array ('object_id', 'id', 'label'),
3188		'label',
3189		$what,
3190		'object_id',
3191		0
3192	);
3193	foreach ($ports as $port)
3194	{
3195		$ret[$port['object_id']]['id'] = $port['object_id'];
3196		$ret[$port['object_id']]['by_port'][$port['id']] = $port['label'];
3197	}
3198	try
3199	{
3200		$db_l2address = l2addressForDatabase ($what);
3201	}
3202	catch (InvalidArgException $e)
3203	{
3204		return $ret;
3205	}
3206	$ports = getSearchResultByField
3207	(
3208		'Port',
3209		array ('object_id', 'id', 'l2address'),
3210		'l2address',
3211		$db_l2address,
3212		'object_id',
3213		2
3214	);
3215	foreach ($ports as $port)
3216	{
3217		$ret[$port['object_id']]['id'] = $port['object_id'];
3218		$ret[$port['object_id']]['by_port'][$port['id']] = $port['l2address'];
3219	}
3220	return $ret;
3221}
3222
3223// search in IPv4 allocations
3224function getObjectIfacesSearchResults ($what)
3225{
3226	$ret = array();
3227	$ifaces4 = getSearchResultByField
3228	(
3229		'IPv4Allocation',
3230		array ('object_id', 'name'),
3231		'name',
3232		$what,
3233		'object_id'
3234	);
3235	$ifaces6 = getSearchResultByField
3236	(
3237		'IPv6Allocation',
3238		array ('object_id', 'name'),
3239		'name',
3240		$what,
3241		'object_id'
3242	);
3243	foreach (array_merge ($ifaces4, $ifaces6) as $row)
3244	{
3245		$ret[$row['object_id']]['id'] = $row['object_id'];
3246		$ret[$row['object_id']]['by_iface'][] = $row['name'];
3247	}
3248	return $ret;
3249}
3250
3251function getObjectNATSearchResults ($what)
3252{
3253	$ret = array();
3254	$ifaces = getSearchResultByField
3255	(
3256		'IPv4NAT',
3257		array ('object_id', 'description'),
3258		'description',
3259		$what,
3260		'object_id'
3261	);
3262	foreach ($ifaces as $row)
3263	{
3264		$ret[$row['object_id']]['id'] = $row['object_id'];
3265		$ret[$row['object_id']]['by_nat'][] = $row['description'];
3266	}
3267	return $ret;
3268}
3269
3270function searchCableIDs ($what)
3271{
3272	$ret = array();
3273	$result = usePreparedSelectBlade
3274	(
3275		'SELECT object_id, cable ' .
3276		'FROM Link INNER JOIN Port ON porta = Port.id OR portb = Port.id ' .
3277		'WHERE cable LIKE ? ORDER BY object_id',
3278		array ("%${what}%")
3279	);
3280	while ($row = $result->fetch (PDO::FETCH_ASSOC))
3281	{
3282		$ret[$row['object_id']]['id'] = $row['object_id'];
3283		$ret[$row['object_id']]['by_cableid'][] = $row['cable'];
3284	}
3285	return $ret;
3286}
3287
3288// This function returns either port ID or NULL for specified arguments.
3289function getPortIDs ($object_id, $port_name)
3290{
3291	$result = usePreparedSelectBlade ('SELECT id FROM Port WHERE object_id = ? AND name = ?', array ($object_id, $port_name));
3292	return reduceSubarraysToColumn ($result->fetchAll (PDO::FETCH_ASSOC), 'id');
3293}
3294
3295// Search in "FQDN" attribute only, and return object ID, when there is exactly
3296// one result found (and NULL in any other case).
3297function searchByMgmtHostname ($string)
3298{
3299	$result = usePreparedSelectBlade ('SELECT object_id FROM AttributeValue WHERE attr_id = 3 AND string_value = ? LIMIT 2', array ($string));
3300	$rows = $result->fetchAll (PDO::FETCH_NUM);
3301	if (count ($rows) == 1)
3302		return $rows[0][0];
3303	unset ($result);
3304
3305	// second attempt: search for FQDN part, separated by dot.
3306	$result = usePreparedSelectBlade ('SELECT object_id FROM AttributeValue WHERE attr_id = 3 AND string_value LIKE ? LIMIT 2', array ("$string.%"));
3307	$rows = $result->fetchAll (PDO::FETCH_NUM);
3308	return count ($rows) == 1 ? $rows[0][0] : NULL;
3309}
3310
3311// returns an array of object ids
3312function searchByAttrValue ($attr_id, $value)
3313{
3314	$type = getAttrType ($attr_id);
3315	if (! isset ($type))
3316		throw new InvalidArgException ('attr_id', $attr_id, "No such attribute");
3317
3318	switch ($type)
3319	{
3320		case 'string':
3321			$field = 'string_value';
3322			break;
3323		case 'float':
3324			$field = 'float_value';
3325			break;
3326		case 'uint':
3327		case 'dict':
3328		case 'date':
3329			$field = 'uint_value';
3330			break;
3331		default:
3332			throw new InvalidArgException ('type', $type);
3333	}
3334
3335	$result = usePreparedSelectBlade ("
3336SELECT object_id FROM AttributeValue
3337WHERE
3338	attr_id = ?
3339	AND $field = ?
3340", array ($attr_id, $value)
3341);
3342	return $result->fetchAll (PDO::FETCH_COLUMN, 0);
3343}
3344
3345// returns user_id
3346// throws an exception if error occured
3347function commitCreateUserAccount ($username, $realname, $password_hash)
3348{
3349	if (! preg_match ('/^[0-9a-f]{40}$/', $password_hash))
3350		throw new InvalidArgException ('password_hash', $password_hash, 'not a 20-octet hex string');
3351	usePreparedInsertBlade
3352	(
3353		'UserAccount',
3354		array
3355		(
3356			'user_name' => $username,
3357			'user_realname' => nullIfEmptyStr ($realname),
3358			'user_password_hash' => $password_hash,
3359		)
3360	);
3361	$user_id = lastInsertID();
3362	lastCreated ('user', $user_id);
3363	return $user_id;
3364}
3365
3366// Update the password only if it is provided.
3367function commitUpdateUserAccount ($id, $new_username, $new_realname, $new_password_hash = '')
3368{
3369	$set_columns = array
3370	(
3371		'user_name' => $new_username,
3372		'user_realname' => nullIfEmptyStr ($new_realname),
3373	);
3374	if ($new_password_hash != '')
3375	{
3376		if (! preg_match ('/^[0-9a-f]{40}$/', $new_password_hash))
3377			throw new InvalidArgException ('new_password_hash', $new_password_hash, 'not a 20-octet hex string');
3378		$set_columns['user_password_hash'] = $new_password_hash;
3379	}
3380	$userinfo = spotEntity ('user', $id);
3381	usePreparedUpdateBlade ('UserAccount', $set_columns, array ('user_id' => $id));
3382	// There is no FK to do this update as user-specific configuration is always local,
3383	// but authentication may be not (the rows in UserConfig may have no matching
3384	// rows in UserAccount).
3385	if ($userinfo['user_name'] != $new_username)
3386		usePreparedUpdateBlade ('UserConfig', array ('user' => $new_username), array('user' => $userinfo['user_name']));
3387}
3388
3389function commitDeleteUserAccount ($id)
3390{
3391	if ($id == 1)
3392		throw new InvalidArgException ('id', $id, 'belongs to the administrator account');
3393	$userinfo = spotEntity ('user', $id);
3394	destroyTagsForEntity ('user', $id);
3395	usePreparedDeleteBlade ('UserConfig', array ('user' => $userinfo['user_name']));
3396	usePreparedDeleteBlade ('UserAccount', array ('user_id' => $id));
3397}
3398
3399// This function returns an array of all port type pairs from PortCompat table.
3400function getPortOIFCompat ($ignore_cache = FALSE)
3401{
3402	static $cache = NULL;
3403	if (! $ignore_cache && isset ($cache))
3404		return $cache;
3405
3406	$query =
3407		"SELECT type1, type2, POI1.oif_name AS type1name, POI2.oif_name AS type2name FROM " .
3408		"PortCompat AS pc INNER JOIN PortOuterInterface AS POI1 ON pc.type1 = POI1.id " .
3409		"INNER JOIN PortOuterInterface AS POI2 ON pc.type2 = POI2.id " .
3410		'ORDER BY type1name, type2name';
3411	$result = usePreparedSelectBlade ($query);
3412	$cache = $result->fetchAll (PDO::FETCH_ASSOC);
3413	return $cache;
3414}
3415
3416function addPortOIFCompat ($type1, $type2)
3417{
3418	return usePreparedExecuteBlade ("INSERT IGNORE INTO PortCompat (type1, type2) VALUES (?, ?),(?, ?)", array ($type1, $type2, $type2, $type1));
3419}
3420
3421function deletePortOIFCompat ($type1, $type2)
3422{
3423	return usePreparedExecuteBlade ("DELETE FROM PortCompat WHERE (type1 = ? AND type2 = ?) OR (type1 = ? AND type2 = ?)", array ($type1, $type2, $type2, $type1));
3424}
3425
3426// Returns an array of all object type pairs from the ObjectParentCompat table.
3427function getObjectParentCompat ()
3428{
3429	$result = usePreparedSelectBlade
3430	(
3431		'SELECT parent_objtype_id, child_objtype_id, d1.dict_value AS parent_name, d2.dict_value AS child_name, ' .
3432		'(SELECT COUNT(*) FROM EntityLink EL ' .
3433		"LEFT JOIN Object PO ON (EL.parent_entity_id = PO.id AND EL.parent_entity_type = 'object') " .
3434		"LEFT JOIN Object CO ON (EL.child_entity_id = CO.id AND EL.child_entity_type = 'object') " .
3435		'WHERE PO.objtype_id = parent_objtype_id AND CO.objtype_id = child_objtype_id) AS count ' .
3436		'FROM ObjectParentCompat AS pc INNER JOIN Dictionary AS d1 ON pc.parent_objtype_id = d1.dict_key ' .
3437		'INNER JOIN Dictionary AS d2 ON pc.child_objtype_id = d2.dict_key ' .
3438		'ORDER BY parent_name, child_name'
3439	);
3440	return $result->fetchAll (PDO::FETCH_ASSOC);
3441}
3442
3443// Used to determine if a type of object may have a parent or not
3444function objectTypeMayHaveParent ($objtype_id)
3445{
3446	$result = usePreparedSelectBlade ('SELECT COUNT(*) FROM ObjectParentCompat WHERE child_objtype_id = ?', array ($objtype_id));
3447	return $result->fetchColumn() > 0;
3448}
3449
3450// Add a pair to the ObjectParentCompat table.
3451function commitSupplementOPC ($parent_objtype_id, $child_objtype_id)
3452{
3453	if ($parent_objtype_id <= 0)
3454		throw new InvalidArgException ('parent_objtype_id', $parent_objtype_id);
3455	if ($child_objtype_id <= 0)
3456		throw new InvalidArgException ('child_objtype_id', $child_objtype_id);
3457	usePreparedInsertBlade
3458	(
3459		'ObjectParentCompat',
3460		array ('parent_objtype_id' => $parent_objtype_id, 'child_objtype_id' => $child_objtype_id)
3461	);
3462}
3463
3464// Remove a pair from the ObjectParentCompat table.
3465function commitReduceOPC ($parent_objtype_id, $child_objtype_id)
3466{
3467	usePreparedDeleteBlade ('ObjectParentCompat', array ('parent_objtype_id' => $parent_objtype_id, 'child_objtype_id' => $child_objtype_id));
3468}
3469
3470function getDictStats ()
3471{
3472	$result = usePreparedSelectBlade
3473	(
3474		"select Chapter.id as chapter_no, Chapter.name as chapter_name, count(dict_key) as wc from " .
3475		"Chapter left join Dictionary on Chapter.id = Dictionary.chapter_id group by Chapter.id"
3476	);
3477	$tc = $tw = $uc = $uw = 0;
3478	while ($row = $result->fetch (PDO::FETCH_ASSOC))
3479	{
3480		$tc++;
3481		$tw += $row['wc'];;
3482		if ($row['chapter_no'] < 10000)
3483			continue;
3484		$uc++;
3485		$uw += $row['wc'];;
3486	}
3487	unset ($result);
3488	$result = usePreparedSelectBlade
3489	(
3490		"select count(object_id) as attrc from RackObject as ro left join " .
3491		"AttributeValue as av on ro.id = av.object_id group by ro.id"
3492	);
3493	$to = $ta = $so = 0;
3494	while ($row = $result->fetch (PDO::FETCH_ASSOC))
3495	{
3496		$to++;
3497		if ($row['attrc'] != 0)
3498		{
3499			$so++;
3500			$ta += $row['attrc'];
3501		}
3502	}
3503	unset ($result);
3504	$ret = array();
3505	$ret['Total chapters in dictionary'] = $tc;
3506	$ret['Total words in dictionary'] = $tw;
3507	$ret['User chapters'] = $uc;
3508	$ret['Words in user chapters'] = $uw;
3509	$ret['Total objects'] = $to;
3510	$ret['Objects with stickers'] = $so;
3511	$ret['Total stickers attached'] = $ta;
3512	return $ret;
3513}
3514
3515function getIPv4Stats ()
3516{
3517	$ret = array();
3518	$subject = array();
3519	$subject[] = array ('q' => 'select count(id) from IPv4Network', 'txt' => 'Networks');
3520	$subject[] = array ('q' => 'select count(ip) from IPv4Address', 'txt' => 'Addresses commented/reserved');
3521	$subject[] = array ('q' => 'select count(ip) from IPv4Allocation', 'txt' => 'Addresses allocated');
3522	$subject[] = array ('q' => 'select count(*) from IPv4NAT', 'txt' => 'NAT rules');
3523	$subject[] = array ('q' => 'select count(id) from IPv4VS', 'txt' => 'Virtual services');
3524	$subject[] = array ('q' => 'select count(id) from IPv4RSPool', 'txt' => 'Real server pools');
3525	$subject[] = array ('q' => 'select count(id) from IPv4RS', 'txt' => 'Real servers');
3526	$subject[] = array ('q' => 'select count(distinct object_id) from IPv4LB', 'txt' => 'Load balancers');
3527
3528	foreach ($subject as $item)
3529	{
3530		$result = usePreparedSelectBlade ($item['q']);
3531		$ret[$item['txt']] = $result->fetchColumn();
3532		unset ($result);
3533	}
3534	return $ret;
3535}
3536
3537function getIPv6Stats ()
3538{
3539	$ret = array();
3540	$subject = array();
3541	$subject[] = array ('q' => 'select count(id) from IPv6Network', 'txt' => 'Networks');
3542	$subject[] = array ('q' => 'select count(ip) from IPv6Address', 'txt' => 'Addresses commented/reserved');
3543	$subject[] = array ('q' => 'select count(ip) from IPv6Allocation', 'txt' => 'Addresses allocated');
3544
3545	foreach ($subject as $item)
3546	{
3547		$result = usePreparedSelectBlade ($item['q']);
3548		$ret[$item['txt']] = $result->fetchColumn();
3549		unset ($result);
3550	}
3551	return $ret;
3552}
3553
3554function getRackspaceStats ()
3555{
3556	$ret = array();
3557	$subject = array();
3558	$subject[] = array ('q' => 'select count(*) from `Row`', 'txt' => 'Rows');
3559	$subject[] = array ('q' => 'select count(*) from Rack', 'txt' => 'Racks');
3560	$subject[] = array ('q' => 'select avg(height) from Rack', 'txt' => 'Average rack height');
3561	$subject[] = array ('q' => 'select sum(height) from Rack', 'txt' => 'Total rack units in field');
3562
3563	foreach ($subject as $item)
3564	{
3565		$result = usePreparedSelectBlade ($item['q']);
3566		$tmp = $result->fetchColumn();
3567		$ret[$item['txt']] = $tmp == '' ? 0 : $tmp;
3568		unset ($result);
3569	}
3570	return $ret;
3571}
3572
3573function getPortsCount ($object_id)
3574{
3575	$result = usePreparedSelectBlade ("SELECT COUNT(id) FROM Port WHERE object_id = ?", array($object_id));
3576	return $result->fetchColumn();
3577}
3578
3579# FIXME: this function is not used any more
3580function commitDeleteChapter ($chapter_no)
3581{
3582	usePreparedDeleteBlade ('Chapter', array ('id' => $chapter_no, 'sticky' => 'no'));
3583}
3584
3585function readChapter ($chapter_id, $style = '')
3586{
3587	$result = usePreparedSelectBlade ('SELECT id FROM Chapter WHERE id = ?', array ($chapter_id));
3588	if (FALSE === $result->fetchColumn())
3589		throw new EntityNotFoundException ('chapter', $chapter_id);
3590	unset ($result);
3591	$result = usePreparedSelectBlade
3592	(
3593		'SELECT dict_key, dict_value AS value FROM Dictionary WHERE chapter_id = ?',
3594		array ($chapter_id)
3595	);
3596	$chapter = array();
3597	while ($row = $result->fetch (PDO::FETCH_ASSOC))
3598	{
3599		if ($style == 'r')
3600			$value = $row['value'];
3601		else
3602		{
3603			parseWikiLink ($row);
3604			$value = ($style == 'a' ? $row['a_value'] : $row['o_value']);
3605		}
3606		$chapter[$row['dict_key']] = $value;
3607	}
3608	// SQL ORDER BY would make no sense in the query above because the rows need
3609	// to be sorted after the wiki link parsing, not before.
3610	// Try to sort after the parsing in the same way as ORDER BY would do.
3611	asort ($chapter, SORT_STRING | SORT_FLAG_CASE);
3612	return $chapter;
3613}
3614
3615// Return refcounters for all given keys of the given chapter.
3616function getChapterRefc ($chapter_id, $keylist)
3617{
3618	$ret = array_fill_keys ($keylist, 0);
3619	switch ($chapter_id)
3620	{
3621	case CHAP_OBJTYPE:
3622		// ObjectType chapter is referenced by AttributeMap and Object tables
3623		$query = 'SELECT dict_key AS uint_value, (SELECT COUNT(*) FROM AttributeMap WHERE objtype_id = dict_key) + ' .
3624			'(SELECT COUNT(*) FROM Object WHERE objtype_id = dict_key) AS refcnt FROM Dictionary WHERE chapter_id = ?';
3625		break;
3626	default:
3627		// Find the list of all assigned values of dictionary-addressed attributes, each with
3628		// chapter/word keyed reference counters.
3629		$query = 'SELECT uint_value, count(object_id) AS refcnt
3630			FROM AttributeMap am
3631			INNER JOIN AttributeValue av ON am.attr_id = av.attr_id
3632			INNER JOIN Object o ON o.id = av.object_id
3633			WHERE am.chapter_id = ? AND o.objtype_id = am.objtype_id
3634			GROUP BY uint_value';
3635		break;
3636	}
3637	$result = usePreparedSelectBlade ($query, array ($chapter_id));
3638	while ($row = $result->fetch (PDO::FETCH_ASSOC))
3639		$ret[$row['uint_value']] = $row['refcnt'];
3640	return $ret;
3641}
3642
3643// Return references counter for each of the given OIF IDs. This includes not
3644// only PortCompat and PortInterfaceCompat but also Port even though the latter
3645// is not based on PortOuterInterface directly.
3646function getPortOIFRefc()
3647{
3648	$result = usePreparedSelectBlade
3649	(
3650		'SELECT POI.id, (' .
3651		'(SELECT COUNT(*) FROM PortCompat WHERE type1 = id) + ' .
3652		'(SELECT COUNT(*) FROM Port WHERE type = POI.id) + ' .
3653		'(SELECT COUNT(*) FROM PortInterfaceCompat WHERE oif_id = POI.id)' .
3654		') AS refcnt FROM PortOuterInterface AS POI'
3655	);
3656	return reduceSubarraysToColumn (reindexById ($result->fetchAll (PDO::FETCH_ASSOC)), 'refcnt');
3657}
3658
3659function getAttrType ($attr_id)
3660{
3661	$result = usePreparedSelectBlade ('SELECT type FROM Attribute WHERE id = ?' , array ($attr_id));
3662	return $result->fetchColumn();
3663}
3664
3665function getObjTypeAttrMap ($objtype_id)
3666{
3667	$result = usePreparedSelectBlade ('SELECT id, type, name, chapter_id, sticky FROM Attribute INNER JOIN AttributeMap ON id = attr_id WHERE objtype_id = ?' , array ($objtype_id));
3668	return reindexById ($result->fetchAll (PDO::FETCH_ASSOC));
3669}
3670
3671// Return a list of all stickers with sticker map applied. Each sticker records will
3672// list all its ways on the map with refcnt set.
3673// The function is pretty heavy, uses temporary tables and scans many rows,
3674// so try to not use it unless it is really necessary
3675function getAttrMap ()
3676{
3677	static $cached_result = NULL;
3678	if (isset ($cached_result))
3679		return $cached_result;
3680
3681	$result = usePreparedSelectBlade
3682	(
3683		'SELECT id, type, name, chapter_id, sticky, (SELECT dict_value FROM Dictionary WHERE dict_key = objtype_id) '.
3684		'AS objtype_name, (SELECT name FROM Chapter WHERE id = chapter_id) ' .
3685		'AS chapter_name, objtype_id, (SELECT COUNT(object_id) FROM AttributeValue AS av INNER JOIN Object AS o ' .
3686		'ON av.object_id = o.id WHERE av.attr_id = Attribute.id AND o.objtype_id = AttributeMap.objtype_id) ' .
3687		'AS refcnt FROM Attribute LEFT JOIN AttributeMap ON id = attr_id ORDER BY Attribute.name, objtype_name'
3688	);
3689	$ret = array();
3690	while ($row = $result->fetch (PDO::FETCH_ASSOC))
3691	{
3692		if (!isset ($ret[$row['id']]))
3693			$ret[$row['id']] = array
3694			(
3695				'id' => $row['id'],
3696				'type' => $row['type'],
3697				'name' => $row['name'],
3698				'application' => array(),
3699			);
3700		if ($row['objtype_id'] == '')
3701			continue;
3702		$application = array
3703		(
3704			'objtype_id' => $row['objtype_id'],
3705			'sticky' => $row['sticky'],
3706			'refcnt' => $row['refcnt'],
3707		);
3708		if ($row['type'] == 'dict')
3709		{
3710			$application['chapter_no'] = $row['chapter_id'];
3711			$application['chapter_name'] = $row['chapter_name'];
3712		}
3713		$ret[$row['id']]['application'][] = $application;
3714	}
3715	$cached_result = $ret;
3716	return $ret;
3717}
3718
3719function commitSupplementAttrMap ($attr_id, $objtype_id, $chapter_no = NULL)
3720{
3721	if ($objtype_id <= 0)
3722		throw new InvalidArgException ('objtype_id', $objtype_id);
3723	if (getAttrType ($attr_id) != 'dict')
3724		$chapter_no = NULL;
3725	elseif ($chapter_no === NULL)
3726		throw new InvalidArgException ('chapter_no', '(NULL)', 'must not be NULL for a [D] attribute');
3727
3728	usePreparedInsertBlade
3729	(
3730		'AttributeMap',
3731		array
3732		(
3733			'attr_id' => $attr_id,
3734			'objtype_id' => $objtype_id,
3735			'chapter_id' => $chapter_no
3736		)
3737	);
3738}
3739
3740function cacheAllObjectsAttributes()
3741{
3742	global $object_attribute_cache;
3743	$object_attribute_cache = fetchAttrsForObjects();
3744}
3745
3746// Fetches a list of attributes for each object in $object_set array.
3747// If $object_set is not set, returns attributes for all objects in DB
3748// Returns an array with object_id keys
3749function fetchAttrsForObjects ($object_set = array())
3750{
3751	$ret = array();
3752	$query =
3753		"select AM.attr_id, A.name as attr_name, A.type as attr_type, C.name as chapter_name, " .
3754		"C.id as chapter_id, AV.uint_value, AV.float_value, AV.string_value, D.dict_value, O.id as object_id from " .
3755		"Object as O left join AttributeMap as AM on O.objtype_id = AM.objtype_id " .
3756		"left join Attribute as A on AM.attr_id = A.id " .
3757		"left join AttributeValue as AV on AV.attr_id = AM.attr_id and AV.object_id = O.id " .
3758		"left join Dictionary as D on D.dict_key = AV.uint_value and AM.chapter_id = D.chapter_id " .
3759		"left join Chapter as C on AM.chapter_id = C.id";
3760	if (count ($object_set))
3761		$query .= ' WHERE O.id IN (' . questionMarks (count ($object_set)) . ')';
3762
3763	$result = usePreparedSelectBlade ($query, $object_set);
3764	while ($row = $result->fetch (PDO::FETCH_ASSOC))
3765	{
3766		$object_id = $row['object_id'];
3767		if (!array_key_exists ($object_id, $ret))
3768			$ret[$object_id] = array();
3769		# Objects with zero attributes also matter due to the LEFT JOIN. Create
3770		# keys for them too to enable negative caching.
3771		if ($row['attr_id'] == NULL)
3772			continue;
3773
3774		$record = array();
3775		$record['id'] = $row['attr_id'];
3776		$record['name'] = $row['attr_name'];
3777		$record['type'] = $row['attr_type'];
3778		switch ($row['attr_type'])
3779		{
3780			case 'dict':
3781				$record['chapter_id'] = $row['chapter_id'];
3782				$record['chapter_name'] = $row['chapter_name'];
3783				$record['key'] = $row['uint_value'];
3784				// fall through
3785			case 'uint':
3786			case 'float':
3787			case 'string':
3788				$record['value'] = $row[$row['attr_type'] . '_value'];
3789				parseWikiLink ($record);
3790				break;
3791			case 'date':
3792				$record['value'] = $row['uint_value'];
3793				break;
3794			default:
3795				throw new RackTablesError ("unexpected attribute type '${row['attr_type']}'", RackTablesError::INTERNAL);
3796		}
3797		$ret[$object_id][$row['attr_id']] = $record;
3798	}
3799	return $ret;
3800}
3801
3802// This function returns all optional attributes for requested object
3803// as an array of records.
3804// Empty array is returned, if there are no attributes found.
3805function getAttrValues ($object_id)
3806{
3807	global $object_attribute_cache;
3808	$ret = array();
3809	if (isset ($object_attribute_cache[$object_id]))
3810		return $object_attribute_cache[$object_id];
3811
3812	$tmp = fetchAttrsForObjects (array ($object_id));
3813	if (isset ($tmp[$object_id]))
3814		$ret = $object_attribute_cache[$object_id] = $tmp[$object_id];
3815	return $ret;
3816}
3817
3818// returns the same data as getAttrValues, but sorts the result array
3819// by the attr_name using SQL server's collation
3820function getAttrValuesSorted ($object_id)
3821{
3822	static $attr_order = NULL;
3823	if (! isset ($attr_order))
3824	{
3825		$attr_order = array();
3826		$result = usePreparedSelectBlade ("SELECT id FROM Attribute ORDER by name");
3827		$i = 0;
3828		foreach ($result->fetchAll (PDO::FETCH_COLUMN, 0) as $attr_id)
3829			$attr_order[$attr_id] = $i++;
3830		unset ($result);
3831	}
3832	return customKsort (getAttrValues ($object_id), $attr_order);
3833}
3834
3835// FIXME: This function causes RTDatabaseError if the attribute is not
3836// enabled for the given object in AttributeMap. It would be better to detect
3837// the mismatch here and throw InvalidArgException instead.
3838function commitUpdateAttrValue ($object_id, $attr_id, $value = '')
3839{
3840	global $object_attribute_cache;
3841	if (isset ($object_attribute_cache[$object_id]))
3842		unset ($object_attribute_cache[$object_id]);
3843	$result = usePreparedSelectBlade
3844	(
3845		'SELECT A.type AS attr_type, AV.attr_id, AV.uint_value, AV.float_value, AV.string_value ' .
3846		'FROM Attribute AS A ' .
3847		'LEFT JOIN AttributeValue AS AV ON A.id = AV.attr_id AND AV.object_id = ? ' .
3848		'WHERE A.id = ?',
3849		array ($object_id, $attr_id)
3850	);
3851	if (! $row = $result->fetch (PDO::FETCH_ASSOC))
3852		throw new InvalidArgException ('attr_id', $attr_id, 'No such attribute');
3853	$attr_type = $row['attr_type'];
3854	unset ($result);
3855	switch ($attr_type)
3856	{
3857		case 'uint':
3858		case 'float':
3859		case 'string':
3860			$column = $attr_type . '_value';
3861			break;
3862		case 'dict':
3863		case 'date':
3864			$column = 'uint_value';
3865			break;
3866		default:
3867			throw new RackTablesError ("Unknown attribute type '${attr_type}' for object_id ${object_id} attr_id ${attr_id}", RackTablesError::INTERNAL);
3868	}
3869	if (isset ($row['attr_id']))
3870	{
3871		// AttributeValue row present in table
3872		$where = array ('object_id' => $object_id, 'attr_id' => $attr_id);
3873		if ($value == '')
3874			usePreparedDeleteBlade ('AttributeValue', $where);
3875		elseif ($row[$column] !== $value)
3876			usePreparedUpdateBlade ('AttributeValue', array ($column => $value), $where);
3877	}
3878	elseif ($value != '')
3879		usePreparedInsertBlade
3880		(
3881			'AttributeValue',
3882			array
3883			(
3884				$column => $value,
3885				'object_id' => $object_id,
3886				'object_tid' => getObjectType ($object_id),
3887				'attr_id' => $attr_id,
3888			)
3889		);
3890}
3891
3892function convertPDOException ($e)
3893{
3894	switch ($e->getCode() . '-' . $e->errorInfo[1])
3895	{
3896	case '23000-1062':
3897	case '23000-1205':
3898		$text = 'such record already exists';
3899		break;
3900	case '23000-1451':
3901	case '23000-1452':
3902		$text = 'foreign key violation';
3903		break;
3904	case 'HY000-1205':
3905		$text = 'lock wait timeout';
3906		break;
3907	case '42000-1142':
3908		return new RTDBTableAccessDenied ($e->getMessage());
3909	default:
3910		return $e;
3911	}
3912	return new RTDatabaseError ($text);
3913}
3914
3915function assertListOfColumnNames ($column_names)
3916{
3917	if (! is_array ($column_names))
3918		throw new InvalidArgException ('column_names', $column_names, 'is not an array');
3919	if (! count ($column_names))
3920		throw new InvalidArgException ('column_names', '(empty array)', 'must not be empty');
3921	foreach ($column_names as $each)
3922		if (! is_string ($each))
3923			throw new InvalidArgException ('column_names', '(array)', 'contains an item that is not a string');
3924		elseif ($each == '')
3925			throw new InvalidArgException ('column_names', '(array)', 'contains an empty string');
3926}
3927
3928// This is a swiss-knife blade to insert a record into a table.
3929// The first argument is table name.
3930// The second argument is an array of "name" => "value" pairs.
3931// returns integer - affected rows count. Throws exception on error
3932function usePreparedInsertBlade ($tablename, $columns)
3933{
3934	global $dbxlink;
3935	$query = "INSERT INTO `${tablename}` SET " . makeSetSQL (array_keys ($columns));
3936	// INSERT INTO `table` SET `c1` = ?, `c2` = ?, `c3` = ?
3937	try
3938	{
3939		$prepared = $dbxlink->prepare ($query);
3940		$prepared->execute (array_values ($columns));
3941		return $prepared->rowCount();
3942	}
3943	catch (PDOException $e)
3944	{
3945		throw convertPDOException ($e);
3946	}
3947}
3948
3949function makeSetSQL ($column_names)
3950{
3951	assertListOfColumnNames ($column_names);
3952	$tmp = array();
3953	// Same syntax works for NULL as well.
3954	foreach ($column_names as $each)
3955		$tmp[] = "`${each}` = ?";
3956	return implode (', ', $tmp);
3957}
3958
3959function makeWhereSQL ($where_columns, $conjunction, &$params)
3960{
3961	if (! in_array (strtoupper ($conjunction), array ('AND', '&&', 'OR', '||', 'XOR')))
3962		throw new InvalidArgException ('conjunction', $conjunction, 'invalid operator');
3963	assertListOfColumnNames (array_keys ($where_columns));
3964	$params = array();
3965	$tmp = array();
3966	foreach ($where_columns as $colname => $colvalue)
3967		if ($colvalue === NULL)
3968			$tmp[] = "`${colname}` IS NULL";
3969		elseif (is_array ($colvalue))
3970		{
3971			// Suppress any string keys to keep array_merge() from overwriting.
3972			$params = array_merge ($params, array_values ($colvalue));
3973			$tmp[] = sprintf ('`%s` IN(%s)', $colname, questionMarks (count ($colvalue)));
3974		}
3975		else
3976		{
3977			$tmp[] = "`${colname}` = ?";
3978			$params[] = $colvalue;
3979		}
3980	return implode (" ${conjunction} ", $tmp);
3981}
3982
3983// This swiss-knife blade deletes any number of records from the specified table
3984// using the specified key names and values.
3985// returns integer - affected rows count. Throws exception on error
3986function usePreparedDeleteBlade ($tablename, $columns, $conjunction = 'AND')
3987{
3988	global $dbxlink;
3989	if (! is_array ($columns) || ! count ($columns))
3990		throw new InvalidArgException ('columns', '(empty array)', 'in this function DELETE must have WHERE');
3991	$query = "DELETE FROM `${tablename}` WHERE " . makeWhereSQL ($columns, $conjunction, $where_values);
3992	try
3993	{
3994		$prepared = $dbxlink->prepare ($query);
3995		$prepared->execute ($where_values);
3996		return $prepared->rowCount();
3997	}
3998	catch (PDOException $e)
3999	{
4000		throw convertPDOException ($e);
4001	}
4002}
4003
4004function usePreparedSelectBlade ($query, $args = array())
4005{
4006	global $dbxlink;
4007	try
4008	{
4009		$prepared = $dbxlink->prepare ($query);
4010		$prepared->execute ($args);
4011		return $prepared;
4012	}
4013	catch (PDOException $e)
4014	{
4015		throw convertPDOException ($e);
4016	}
4017}
4018
4019// returns integer - affected rows count. Throws exception on error
4020function usePreparedUpdateBlade ($tablename, $set_columns, $where_columns, $conjunction = 'AND')
4021{
4022	global $dbxlink;
4023	if (! is_array ($set_columns) || ! count ($set_columns))
4024		throw new InvalidArgException ('set_columns', '(empty array)', 'UPDATE must have SET');
4025	if (! is_array ($where_columns) || ! count ($where_columns))
4026		throw new InvalidArgException ('where_columns', '(empty array)', 'in this function UPDATE must have WHERE');
4027	$query = "UPDATE `${tablename}` SET " . makeSetSQL (array_keys ($set_columns));
4028	$query .= ' WHERE ' . makeWhereSQL ($where_columns, $conjunction, $where_values);
4029	try
4030	{
4031		$prepared = $dbxlink->prepare ($query);
4032		$prepared->execute (array_merge (array_values ($set_columns), $where_values));
4033		return $prepared->rowCount();
4034	}
4035	catch (PDOException $e)
4036	{
4037		throw convertPDOException ($e);
4038	}
4039}
4040
4041// Prepare and execute the statement with parameters
4042// returns integer - affected rows count. Throws exception on error
4043function usePreparedExecuteBlade ($query, $args = array())
4044{
4045	global $dbxlink;
4046	try
4047	{
4048		$prepared = $dbxlink->prepare ($query);
4049		$prepared->execute ($args);
4050		return $prepared->rowCount();
4051	}
4052	catch (PDOException $e)
4053	{
4054		throw convertPDOException ($e);
4055	}
4056}
4057
4058function loadConfigCache ()
4059{
4060	$result = usePreparedSelectBlade ('SELECT varname, varvalue, vartype, is_hidden, emptyok, description, is_userdefined FROM Config ORDER BY varname');
4061	return reindexById ($result->fetchAll (PDO::FETCH_ASSOC), 'varname');
4062}
4063
4064function loadUserConfigCache ($username)
4065{
4066	if ($username == '')
4067		throw new InvalidArgException ('username', $username, 'must not be empty');
4068	$result = usePreparedSelectBlade ('SELECT varname, varvalue FROM UserConfig WHERE user = ?', array ($username));
4069	return reindexById ($result->fetchAll (PDO::FETCH_ASSOC), 'varname');
4070}
4071
4072function loadRackThumbCache ($rack_id)
4073{
4074	$result = usePreparedSelectBlade ('SELECT thumb_data FROM RackThumbnail WHERE rack_id = ?', array ($rack_id));
4075	$row = $result->fetch (PDO::FETCH_ASSOC);
4076	return $row ? $row['thumb_data'] : NULL;
4077}
4078
4079function saveRackThumbCache ($rack_id, $thumb_data)
4080{
4081	global $dbxlink;
4082	try
4083	{
4084		$query = $dbxlink->prepare ('REPLACE INTO RackThumbnail SET rack_id = ?, thumb_data = ?');
4085		$query->bindParam (1, $rack_id);
4086		$query->bindParam (2, $thumb_data, PDO::PARAM_LOB);
4087		return $query->execute();
4088	}
4089	catch (PDOException $e)
4090	{
4091		throw convertPDOException ($e);
4092	}
4093}
4094
4095function executeAutoPorts ($object_id)
4096{
4097	foreach (getAutoPorts (spotEntity ('object', $object_id)) as $autoport)
4098		commitAddPort ($object_id, $autoport['name'], $autoport['type'], '', '');
4099}
4100
4101// Return only explicitly listed tags, the rest of the chain will be
4102// generated/deducted later at higher levels.
4103// Result is a chain: randomly indexed taginfo list.
4104function loadEntityTags ($entity_realm, $entity_id)
4105{
4106	$result = usePreparedSelectBlade
4107	(
4108		"SELECT tt.id, tag FROM " .
4109		"TagStorage AS ts INNER JOIN TagTree AS tt ON ts.tag_id = tt.id " .
4110		"WHERE entity_realm = ? AND entity_id = ? " .
4111		"ORDER BY tt.tag",
4112		array ($entity_realm, $entity_id)
4113	);
4114	return reindexById ($result->fetchAll (PDO::FETCH_ASSOC));
4115}
4116
4117function cacheDictAttrValues()
4118{
4119	global $dict_attr_cache;
4120	$dict_attr_cache = array();
4121	$result = usePreparedSelectBlade ("
4122SELECT
4123	AV.attr_id,
4124	AV.uint_value,
4125	AV.object_id
4126FROM
4127	AttributeValue as AV
4128	JOIN  Attribute as A ON AV.attr_id = A.id
4129WHERE
4130	A.type = 'dict'
4131	AND uint_value IS NOT NULL
4132");
4133	while ($row = $result->fetch (PDO::FETCH_ASSOC))
4134		$dict_attr_cache[$row['object_id']][$row['attr_id']] = $row['uint_value'];
4135}
4136
4137# Universal autotags generator, a complementing function for loadEntityTags().
4138# Bypass key isn't strictly typed, but interpreted depending on the realm.
4139function generateEntityAutoTags ($cell)
4140{
4141	global $dict_attr_cache;
4142	$ret = array();
4143	if (! array_key_exists ('realm', $cell))
4144		throw new InvalidArgException ('cell', '(array)', 'malformed structure');
4145	switch ($cell['realm'])
4146	{
4147		case 'location':
4148			$ret[] = array ('tag' => '$locationid_' . $cell['id']);
4149			$ret[] = array ('tag' => '$any_location');
4150			break;
4151		case 'row':
4152			$ret[] = array ('tag' => '$rowid_' . $cell['id']);
4153			$ret[] = array ('tag' => '$any_row');
4154			break;
4155		case 'rack':
4156			$ret[] = array ('tag' => '$rackid_' . $cell['id']);
4157			$ret[] = array ('tag' => '$any_rack');
4158			break;
4159		case 'object':
4160			$ret[] = array ('tag' => '$id_' . $cell['id']);
4161			$ret[] = array ('tag' => '$typeid_' . $cell['objtype_id']);
4162			$ret[] = array ('tag' => '$any_object');
4163			if ($cell['name'] == '')
4164				$ret[] = array ('tag' => '$nameless');
4165			if (validTagName ('$cn_' . $cell['name'], TRUE))
4166				$ret[] = array ('tag' => '$cn_' . $cell['name']);
4167			if ($cell['rack_id'] == '' && $cell['container_id'] == '')
4168				$ret[] = array ('tag' => '$unmounted');
4169			if (!$cell['nports'])
4170				$ret[] = array ('tag' => '$portless');
4171			if ($cell['asset_no'] == '')
4172				$ret[] = array ('tag' => '$no_asset_tag');
4173			if (isset ($cell['8021q_domain_id']))
4174			{
4175				$ret[] = array ('tag' => '$runs_8021Q');
4176				$ret[] = array ('tag' => '$8021Q_domain_' . $cell['8021q_domain_id']);
4177				if (isset ($cell['8021q_template_id']))
4178					$ret[] = array ('tag' => '$8021Q_tpl_' . $cell['8021q_template_id']);
4179			}
4180
4181			# dictionary attribute autotags '$attr_X_Y'
4182			$dict_attrs = array();
4183			if (isset ($dict_attr_cache))
4184			{
4185				if (isset ($dict_attr_cache[$cell['id']]))
4186					$dict_attrs = $dict_attr_cache[$cell['id']];
4187			}
4188			else
4189			{
4190				foreach (getAttrValues($cell['id']) as $attr_id => $attr_record)
4191					if (isset ($attr_record['key']))
4192						$dict_attrs[$attr_id] = $attr_record['key'];
4193			}
4194			foreach ($dict_attrs as $attr_id => $key)
4195				$ret[] = array ('tag' => "\$attr_{$attr_id}_{$key}");
4196			break;
4197		case 'ipv4net':
4198			// v4-only rules
4199			$ret[] = array ('tag' => '$ip4net-' . str_replace ('.', '-', $cell['ip']) . '-' . $cell['mask']);
4200		case 'ipv6net':
4201			// common (v4 & v6) rules
4202			$ver = $cell['realm'] == 'ipv4net' ? 4 : 6;
4203			$ret[] = array ('tag' => "\$ip${ver}netid_" . $cell['id']);
4204			$ret[] = array ('tag' => "\$any_ip${ver}net");
4205			$ret[] = array ('tag' => '$any_net');
4206
4207			$ret[] = array ('tag' => '$masklen_eq_' . $cell['mask']);
4208
4209			if ($cell['vlanc'])
4210				$ret[] = array ('tag' => '$runs_8021Q');
4211
4212			foreach ($cell['8021q'] as $vlan_info)
4213				$ret[] = array ('tag' => '$vlan_' . $vlan_info['vlan_id']);
4214
4215			foreach (array_keys ($cell['spare_ranges']) as $mask)
4216				$ret[] = array ('tag' => '$spare_' . $mask);
4217
4218			if ($cell['kidc'] > 0)
4219				$ret[] = array ('tag' => '$aggregate');
4220			break;
4221		case 'ipv4vs':
4222			$ret[] = array ('tag' => '$ipvsid_' . $cell['id']);
4223			if (strlen ($cell['vip_bin']) == 16)
4224				$ret[] = array ('tag' => '$any_ipv6vs');
4225			else
4226				$ret[] = array ('tag' => '$any_ipv4vs');
4227			$ret[] = array ('tag' => '$any_vs');
4228			if ($cell['refcnt'] == 0)
4229				$ret[] = array ('tag' => '$unused');
4230			$ret[] = array ('tag' => '$type_' . strtolower ($cell['proto'])); // $type_tcp, $type_udp or $type_mark
4231			break;
4232		case 'ipvs':
4233			$ret[] = array ('tag' => '$ipvsid_' . $cell['id']);
4234			$ret[] = array ('tag' => '$any_vs');
4235			break;
4236		case 'ipv4rspool':
4237			$ret[] = array ('tag' => '$ipv4rspid_' . $cell['id']);
4238			$ret[] = array ('tag' => '$any_ipv4rsp');
4239			$ret[] = array ('tag' => '$any_rsp');
4240			if ($cell['refcnt'] == 0)
4241				$ret[] = array ('tag' => '$unused');
4242			break;
4243		case 'user':
4244			# {$username_XXX} autotag is generated always, but {$userid_XXX}
4245			# appears only for accounts that exist in local database.
4246			$ret[] = array ('tag' => '$username_' . $cell['user_name']);
4247			if (isset ($cell['user_id']))
4248				$ret[] = array ('tag' => '$userid_' . $cell['user_id']);
4249			break;
4250		case 'file':
4251			$ret[] = array ('tag' => '$fileid_' . $cell['id']);
4252			$ret[] = array ('tag' => '$any_file');
4253			break;
4254		case 'vst':
4255			$ret[] = array ('tag' => '$vstid_' . $cell['id']);
4256			$ret[] = array ('tag' => '$any_vst');
4257			break;
4258		default:
4259			throw new InvalidArgException ('cell', '(array)', 'this input does not belong here');
4260			break;
4261	}
4262	# {$untagged} doesn't apply to users
4263	switch ($cell['realm'])
4264	{
4265		case 'rack':
4266		case 'object':
4267		case 'ipv4net':
4268		case 'ipv6net':
4269		case 'ipv4vs':
4270		case 'ipv4rspool':
4271		case 'file':
4272		case 'vst':
4273			if (!count ($cell['etags']))
4274				$ret[] = array ('tag' => '$untagged');
4275			break;
4276		default:
4277			break;
4278	}
4279	return $ret;
4280}
4281
4282// Return a tag chain with all DB tags on it. ORDER BY is important as it
4283// enables treeFromList() and sortTree() to do their jobs properly. Doing
4284// the same sorting in PHP is possible but complicated and may deliver
4285// results different from MySQL.
4286function getTagList ($extra_sql = '')
4287{
4288	$result = usePreparedSelectBlade
4289	(
4290		'SELECT id, parent_id, is_assignable, tag, LPAD(HEX(color), 6, "0") AS color, description ' .
4291		"FROM TagTree ORDER BY tag ${extra_sql}"
4292	);
4293	return reindexById ($result->fetchAll (PDO::FETCH_ASSOC));
4294}
4295
4296function getTagUsage ($ignore_cache = FALSE)
4297{
4298	global $taglist;
4299	static $ret = NULL;
4300	if (isset ($ret) && ! $ignore_cache)
4301		return $ret;
4302	$ret = array();
4303
4304	foreach ($taglist as $id => $taginfo)
4305		$ret[$id] = $taginfo + array('refcnt' => array('total' => 0));
4306
4307	$result = usePreparedSelectBlade ("SELECT entity_realm AS realm, tag_id AS id, count(*) AS refcnt FROM TagStorage GROUP BY tag_id, entity_realm");
4308	while ($row = $result->fetch(PDO::FETCH_ASSOC))
4309	{
4310		$ret[$row['id']]['refcnt'][$row['realm']] = $row['refcnt'];
4311		$ret[$row['id']]['refcnt']['total'] += $row['refcnt'];
4312		// introduce the 'pseudo'-realm 'ipnet' which combines 'ipv4net' and 'ipv6net' realms.
4313		if ($row['realm'] == 'ipv4net' || $row['realm'] == 'ipv6net')
4314			if (isset ($ret[$row['id']]['refcnt']['ipnet']))
4315				$ret[$row['id']]['refcnt']['ipnet'] += $row['refcnt'];
4316			else
4317				$ret[$row['id']]['refcnt']['ipnet'] = $row['refcnt'];
4318	}
4319	return $ret;
4320}
4321
4322// Drop the whole chain stored.
4323function destroyTagsForEntity ($entity_realm, $entity_id)
4324{
4325	usePreparedDeleteBlade ('TagStorage', array ('entity_realm' => $entity_realm, 'entity_id' => $entity_id));
4326	if ($entity_realm == 'rack')
4327		usePreparedDeleteBlade ('RackThumbnail', array ('rack_id' => $entity_id));
4328	elseif ($entity_realm == 'object' && count ($rack_ids = getResidentRackIDs ($entity_id)))
4329		usePreparedDeleteBlade ('RackThumbnail', array ('rack_id' => $rack_ids));
4330}
4331
4332// Drop only one record. This operation doesn't involve retossing other tags, unlike when adding.
4333// FIXME: this function could be used by 3rd-party scripts, dismiss it at some later point,
4334// but not now.
4335function deleteTagForEntity ($entity_realm, $entity_id, $tag_id)
4336{
4337	return usePreparedDeleteBlade ('TagStorage', array ('entity_realm' => $entity_realm, 'entity_id' => $entity_id, 'tag_id' => $tag_id));
4338}
4339
4340// A tag's parent may not be one of its children, the tag itself or a tag
4341// that does not belong to the forest of rooted trees because of a cycle.
4342function commitUpdateTag ($tag_id, $tag_name, $parent_id, $is_assignable, $color = NULL)
4343{
4344	global $dbxlink;
4345	global $taglist;
4346	$dbxlink->beginTransaction();
4347	try
4348	{
4349		// Use the copy from within the transaction.
4350		$local_taglist = addTraceToNodes (getTagList ('FOR UPDATE'));
4351		assertValidParentId ($local_taglist, $tag_id, $parent_id);
4352		$db_color = HTMLColorForDatabase ($color);
4353		usePreparedUpdateBlade
4354		(
4355			'TagTree',
4356			array
4357			(
4358				'tag' => $tag_name,
4359				'parent_id' => nullIfZero ($parent_id),
4360				'is_assignable' => $is_assignable,
4361				'color' => $db_color
4362			),
4363			array ('id' => $tag_id)
4364		);
4365		if ($db_color !== HTMLColorForDatabase ($local_taglist[$tag_id]['color']))
4366			// Remove only those rack thumbnails that include any objects tagged with
4367			// the current tag (this does not include zero-U associations) or that
4368			// represent racks tagged with the current tag.
4369			usePreparedExecuteBlade
4370			(
4371				'DELETE FROM RackThumbnail WHERE ' .
4372				'rack_id IN(SELECT DISTINCT rack_id FROM TagStorage INNER JOIN RackSpace ON entity_id = object_id ' .
4373				'  WHERE entity_realm = "object" AND tag_id = ?)' .
4374				'OR rack_id IN(SELECT entity_id FROM TagStorage WHERE entity_realm = "rack" AND tag_id = ?)',
4375				array ($tag_id, $tag_id)
4376			);
4377		$taglist = addTraceToNodes (getTagList());
4378		$dbxlink->commit();
4379	}
4380	catch (PDOException $pe)
4381	{
4382		$dbxlink->rollBack();
4383		throw convertPDOException ($pe);
4384	}
4385	catch (Exception $e)
4386	{
4387		$dbxlink->rollBack();
4388		throw $e;
4389	}
4390}
4391
4392// Push a record into TagStorage unconditionally.
4393function addTagForEntity ($realm, $entity_id, $tag_id)
4394{
4395	global $SQLSchema;
4396	global $remote_username;
4397	if (! array_key_exists ($realm, $SQLSchema))
4398		throw new InvalidArgException ('realm', $realm);
4399	// spotEntity ($realm, $entity_id) would be a more expensive way
4400	// to validate two parameters
4401	usePreparedExecuteBlade
4402	(
4403		'INSERT INTO TagStorage (entity_realm, entity_id, tag_id, user, date) VALUES (?, ?, ?, ?, NOW())',
4404		array
4405		(
4406			$realm,
4407			$entity_id,
4408			$tag_id,
4409			$remote_username,
4410		)
4411	);
4412}
4413
4414// When $replace == FALSE:
4415// Add records into TagStorage if this makes sense (IOW, they don't appear
4416// on the implicit list already). Then remove any other records that
4417// appear on the "implicit" side of the chain. This will make sure
4418// that both the tag base is still minimal and all requested tags appear on
4419// the resulting tag chain.
4420//
4421// When $replace == TRUE:
4422// Replace the current tag chain with the given tag chain (same implicit tag
4423// suppression applies as above).
4424//
4425// Return TRUE if any changes were committed.
4426function rebuildTagChainForEntity ($realm, $entity_id, $extrachain = array(), $replace = FALSE)
4427{
4428	// Put the current explicit sub-chain into a buffer and merge all tags from
4429	// the extra chain that aren't there yet.
4430	$oldchain = array();
4431	$newchain = array();
4432	foreach (loadEntityTags ($realm, $entity_id) as $oldtag)
4433		$oldchain[$oldtag['id']] = $oldtag;
4434	$tmpchain = $replace ? array() : $oldchain;
4435	foreach ($extrachain as $extratag)
4436		$tmpchain[$extratag['id']] = $extratag;
4437	// minimize the working buffer
4438	foreach (getExplicitTagsOnly ($tmpchain) as $taginfo)
4439		$newchain[$taginfo['id']] = $taginfo;
4440
4441	if (array_values_same (array_keys ($oldchain), array_keys ($newchain)))
4442		return FALSE;
4443	foreach (array_diff (array_keys($oldchain), array_keys ($newchain)) as $tag_id)
4444		deleteTagForEntity ($realm, $entity_id, $tag_id);
4445	foreach (array_diff (array_keys($newchain), array_keys ($oldchain)) as $tag_id)
4446		addTagForEntity ($realm, $entity_id, $tag_id);
4447
4448	// remove Rack thumbnail if Rack or Object tag changes
4449	if ($realm == 'rack')
4450		usePreparedDeleteBlade ('RackThumbnail', array ('rack_id' => $entity_id));
4451	elseif ($realm == 'object' && count ($rack_ids = getResidentRackIDs ($entity_id)))
4452		usePreparedDeleteBlade ('RackThumbnail', array ('rack_id' => $rack_ids));
4453
4454	return TRUE;
4455}
4456
4457// Replace any tags already assigned to the target entity.
4458function produceTagsForNewRecord ($realm, $tagidlist, $record_id)
4459{
4460	rebuildTagChainForEntity ($realm, $record_id, buildTagChainFromIds ($tagidlist), TRUE);
4461}
4462
4463function createIPv4Prefix ($range = '', $name = '', $is_connected = FALSE, $taglist = array(), $vlan_ck = NULL)
4464{
4465	// $range is in x.x.x.x/x format, split into ip/mask vars
4466	$rangeArray = explode('/', $range);
4467	if (count ($rangeArray) != 2)
4468		throw new InvalidArgException ('range', $range, 'Invalid IPv4 prefix');
4469	$ip = $rangeArray[0];
4470	$mask = $rangeArray[1];
4471	$forbidden_ranges = array
4472	(
4473		constructIPRange ("\0\0\0\0", 8), # 0.0.0.0/8
4474		constructIPRange ("\xF0\0\0\0", 4), # 240.0.0.0/4
4475	);
4476	$net = constructIPRange (ip4_parse ($ip), $mask);
4477	foreach ($forbidden_ranges as $invalid_net)
4478		if (IPNetContainsOrEqual ($invalid_net, $net))
4479			throw new InvalidArgException ('range', $range, 'Reserved IPv4 network');
4480
4481	usePreparedInsertBlade
4482	(
4483		'IPv4Network',
4484		array
4485		(
4486			'ip' => ip4_bin2db ($net['ip_bin']),
4487			'mask' => $mask,
4488			'name' => $name
4489		)
4490	);
4491	$network_id = lastInsertID();
4492	lastCreated ('ipv4net', $network_id);
4493
4494	if ($is_connected && $mask < 31)
4495	{
4496		updateV4Address ($net['ip_bin'], 'network', 'yes');
4497		updateV4Address (ip_last ($net), 'broadcast', 'yes');
4498	}
4499	produceTagsForNewRecord ('ipv4net', $taglist, $network_id);
4500	if ($vlan_ck != NULL)
4501		commitSupplementVLANIPv4 ($vlan_ck, $network_id);
4502	return $network_id;
4503}
4504
4505function createIPv6Prefix ($range = '', $name = '', $is_connected = FALSE, $taglist = array(), $vlan_ck = NULL)
4506{
4507	// $range is in aaa0:b::c:d/x format, split into ip/mask vars
4508	$rangeArray = explode ('/', $range);
4509	if (count ($rangeArray) != 2)
4510		throw new InvalidArgException ('range', $range, 'Invalid IPv6 prefix');
4511	$ip = $rangeArray[0];
4512	$mask = $rangeArray[1];
4513	$net = constructIPRange (ip6_parse ($ip), $mask);
4514	usePreparedInsertBlade
4515	(
4516		'IPv6Network',
4517		array
4518		(
4519			'ip' => $net['ip_bin'],
4520			'last_ip' => ip_last ($net),
4521			'mask' => $mask,
4522			'name' => $name
4523		)
4524	);
4525	$network_id = lastInsertID();
4526	lastCreated ('ipv6net', lastInsertID());
4527
4528	# RFC3513 2.6.1 - Subnet-Router anycast
4529	if ($is_connected)
4530		updateV6Address ($net['ip_bin'], 'Subnet-Router anycast', 'yes');
4531	produceTagsForNewRecord ('ipv6net', $taglist, $network_id);
4532	if ($vlan_ck != NULL)
4533		commitSupplementVLANIPv6 ($vlan_ck, $network_id);
4534	return $network_id;
4535}
4536
4537// FIXME: This function doesn't wipe relevant records from IPv4Address table.
4538function destroyIPv4Prefix ($id)
4539{
4540	releaseFiles ('ipv4net', $id);
4541	usePreparedDeleteBlade ('IPv4Network', array ('id' => $id));
4542	destroyTagsForEntity ('ipv4net', $id);
4543}
4544
4545// FIXME: This function doesn't wipe relevant records from IPv6Address table.
4546function destroyIPv6Prefix ($id)
4547{
4548	releaseFiles ('ipv6net', $id);
4549	usePreparedDeleteBlade ('IPv6Network', array ('id' => $id));
4550	destroyTagsForEntity ('ipv6net', $id);
4551}
4552
4553function loadScript ($name)
4554{
4555	$result = usePreparedSelectBlade ("SELECT script_text FROM Script WHERE script_name = ?", array ($name));
4556	return nullIfFalse ($result->fetchColumn());
4557}
4558
4559function saveScript ($name, $text)
4560{
4561	if ($name == '')
4562		throw new InvalidArgException ('name', $name, 'must not be empty');
4563	if (!isset ($text))
4564		return deleteScript ($name);
4565	return usePreparedExecuteBlade
4566	(
4567		'INSERT INTO Script (script_name, script_text) VALUES (?, ?) ' .
4568		'ON DUPLICATE KEY UPDATE script_text=?',
4569		array ($name, $text, $text)
4570	);
4571}
4572
4573function deleteScript ($name)
4574{
4575	if ($name == '')
4576		throw new InvalidArgException ('name', $name);
4577	return usePreparedDeleteBlade ('Script', array ('script_name' => $name));
4578}
4579
4580function newPortForwarding ($object_id, $localip_bin, $localport, $remoteip_bin, $remoteport, $proto, $description)
4581{
4582	if (NULL === getIPv4AddressNetworkId ($localip_bin))
4583		throw new InvalidArgException ('localip_bin', ip4_format ($localip_bin), 'address does not belong to a known network');
4584	if (NULL === getIPv4AddressNetworkId ($remoteip_bin))
4585		throw new InvalidArgException ('remoteip_bin', ip4_format ($remoteip_bin), 'address does not belong to a known network');
4586	if ( $proto == "ALL" )
4587	{
4588		$localport = 0;
4589		$remoteport = 0;
4590	}
4591	else
4592	{
4593		if ($localport <= 0 || $localport >= 65536)
4594			throw new InvalidArgException ('localport', $localport, 'Invaild port');
4595		if ($remoteport <= 0 || $remoteport >= 65536)
4596			throw new InvalidArgException ('remoteport', $remoteport, 'Invaild port');
4597	}
4598
4599	return usePreparedInsertBlade
4600	(
4601		'IPv4NAT',
4602		array
4603		(
4604			'object_id' => $object_id,
4605			'localip' => ip4_bin2db ($localip_bin),
4606			'localport' => $localport,
4607			'remoteip' => ip4_bin2db ($remoteip_bin),
4608			'remoteport' => $remoteport,
4609			'proto' => $proto,
4610			'description' => $description,
4611		)
4612	);
4613}
4614
4615function deletePortForwarding ($object_id, $localip_bin, $localport, $remoteip_bin, $remoteport, $proto)
4616{
4617	return usePreparedDeleteBlade
4618	(
4619		'IPv4NAT',
4620		array
4621		(
4622			'object_id' => $object_id,
4623			'localip' => ip4_bin2db ($localip_bin),
4624			'localport' => $localport,
4625			'remoteip' => ip4_bin2db ($remoteip_bin),
4626			'remoteport' => $remoteport,
4627			'proto' => $proto,
4628		)
4629	);
4630}
4631
4632function updatePortForwarding ($object_id, $localip_bin, $localport, $remoteip_bin, $remoteport, $proto, $description)
4633{
4634	return usePreparedUpdateBlade
4635	(
4636		'IPv4NAT',
4637		array ('description' => $description),
4638		array
4639		(
4640			'object_id' => $object_id,
4641			'localip' => ip4_bin2db ($localip_bin),
4642			'localport' => $localport,
4643			'remoteip' => ip4_bin2db ($remoteip_bin),
4644			'remoteport' => $remoteport,
4645			'proto' => $proto,
4646		)
4647	);
4648}
4649
4650function getNATv4CountForObject ($object_id)
4651{
4652	$result = usePreparedSelectBlade ('SELECT COUNT(*) FROM IPv4NAT WHERE object_id = ?', array ($object_id));
4653	return $result->fetchColumn();
4654}
4655
4656function getNATv4ForObject ($object_id)
4657{
4658	$ret = array();
4659	$ret['out'] = array();
4660	$ret['in'] = array();
4661	$result = usePreparedSelectBlade
4662	(
4663		"select ".
4664		"proto, ".
4665		"localip, ".
4666		"localport, ".
4667		"remoteip, ".
4668		"remoteport, ".
4669		"ipa1.name as local_addr_name, " .
4670		"ipa2.name as remote_addr_name, " .
4671		"description ".
4672		"from IPv4NAT ".
4673		"left join IPv4Address as ipa1 on IPv4NAT.localip = ipa1.ip " .
4674		"left join IPv4Address as ipa2 on IPv4NAT.remoteip = ipa2.ip " .
4675		"where object_id=? ".
4676		"order by localip, localport, proto, remoteip, remoteport",
4677		array ($object_id)
4678	);
4679	while ($row = $result->fetch (PDO::FETCH_ASSOC))
4680	{
4681		$row['localip_bin'] = ip4_int2bin ($row['localip']);
4682		$row['localip'] = ip_format ($row['localip_bin']);
4683		$row['remoteip_bin'] = ip4_int2bin ($row['remoteip']);
4684		$row['remoteip'] = ip_format ($row['remoteip_bin']);
4685		$ret['out'][] = $row;
4686	}
4687	unset ($result);
4688
4689	$result = usePreparedSelectBlade
4690	(
4691		"select ".
4692		"proto, ".
4693		"localip, ".
4694		"localport, ".
4695		"remoteip, ".
4696		"remoteport, ".
4697		"IPv4NAT.object_id as object_id, ".
4698		"Object.name as object_name, ".
4699		"description ".
4700		"from ((IPv4NAT join IPv4Allocation on remoteip=IPv4Allocation.ip) join Object on IPv4NAT.object_id=Object.id) ".
4701		"where IPv4Allocation.object_id=? ".
4702		"order by remoteip, remoteport, proto, localip, localport",
4703		array ($object_id)
4704	);
4705	while ($row = $result->fetch (PDO::FETCH_ASSOC))
4706	{
4707		$row['localip_bin'] = ip4_int2bin ($row['localip']);
4708		$row['localip'] = ip_format ($row['localip_bin']);
4709		$row['remoteip_bin'] = ip4_int2bin ($row['remoteip']);
4710		$row['remoteip'] = ip_format ($row['remoteip_bin']);
4711		$ret['in'][] = $row;
4712	}
4713	return $ret;
4714}
4715
4716// Return a list of files that are not linked to the specified record. This list
4717// will be used by printSelect().
4718function getAllUnlinkedFiles ($entity_type, $entity_id)
4719{
4720	$result = usePreparedSelectBlade
4721	(
4722		'SELECT id, name, SUBSTR(comment FROM 1 FOR 128) AS comment FROM File ' .
4723		'WHERE id NOT IN (SELECT file_id FROM FileLink WHERE entity_type = ? AND entity_id = ?) ' .
4724		'ORDER BY name, id',
4725		array ($entity_type, $entity_id)
4726	);
4727	return $result->fetchAll (PDO::FETCH_ASSOC);
4728}
4729
4730// FIXME: return a standard cell list, so upper layer can iterate over
4731// it conveniently.
4732function getFilesOfEntity ($entity_type, $entity_id)
4733{
4734	$result = usePreparedSelectBlade
4735	(
4736		'SELECT FileLink.file_id as id, FileLink.id AS link_id, name, type, size, ctime, mtime, atime, comment ' .
4737		'FROM FileLink LEFT JOIN File ON FileLink.file_id = File.id ' .
4738		'WHERE FileLink.entity_type = ? AND FileLink.entity_id = ? ORDER BY name',
4739		array ($entity_type, $entity_id)
4740	);
4741	return reindexById ($result->fetchAll (PDO::FETCH_ASSOC));
4742}
4743
4744function getFile ($file_id)
4745{
4746	$result = usePreparedSelectBlade
4747	(
4748		'SELECT id, name, type, size, ctime, mtime, atime, contents, comment ' .
4749		'FROM File WHERE id = ?',
4750		array ($file_id)
4751	);
4752	if (($row = $result->fetch (PDO::FETCH_ASSOC)) == NULL)
4753		// FIXME: isn't this repeating the code already in spotEntity()?
4754		throw new EntityNotFoundException ('file', $file_id);
4755	return $row;
4756}
4757
4758function getFileCache ($file_id)
4759{
4760	$result = usePreparedSelectBlade
4761	(
4762		'SELECT File.thumbnail FROM File ' .
4763		'WHERE File.id = ? and File.thumbnail IS NOT NULL',
4764		array ($file_id)
4765	);
4766	return $result->fetchColumn();
4767}
4768
4769function commitAddFileCache ($file_id, $contents)
4770{
4771	global $dbxlink;
4772	try
4773	{
4774		$query = $dbxlink->prepare ('UPDATE File SET thumbnail = ? WHERE id = ?');
4775		$query->bindParam (1, $contents, PDO::PARAM_LOB);
4776		$query->bindParam (2, $file_id);
4777		return $query->execute();
4778	}
4779	catch (PDOException $e)
4780	{
4781		throw convertPDOException ($e);
4782	}
4783}
4784
4785function getFileLinks ($file_id)
4786{
4787	$result = usePreparedSelectBlade
4788	(
4789		'SELECT id, entity_type, entity_id FROM FileLink ' .
4790		'WHERE file_id = ? ORDER BY entity_type, entity_id',
4791		array ($file_id)
4792	);
4793	return reindexById ($result->fetchAll (PDO::FETCH_ASSOC));
4794}
4795
4796function getFileStats ()
4797{
4798	$result = usePreparedSelectBlade ('SELECT entity_type, COUNT(*) AS count FROM FileLink GROUP BY entity_type');
4799	$ret = array();
4800	while ($row = $result->fetch (PDO::FETCH_ASSOC))
4801		if ($row['count'] > 0)
4802			$ret["Links in realm '${row['entity_type']}'"] = $row['count'];
4803	unset ($result);
4804
4805	// Find number of files without any linkage
4806	$result = usePreparedSelectBlade
4807	(
4808		'SELECT COUNT(*) ' .
4809		'FROM File ' .
4810		'WHERE id NOT IN (SELECT file_id FROM FileLink)'
4811	);
4812	$ret["Unlinked files"] = $result->fetchColumn ();
4813	unset ($result);
4814
4815	// Find total number of files
4816	$result = usePreparedSelectBlade ('SELECT COUNT(*) FROM File');
4817	$ret["Total files"] = $result->fetchColumn ();
4818
4819	return $ret;
4820}
4821
4822// returns file id
4823// throws an exception if error occured
4824function commitAddFile ($name, $type, $contents, $comment)
4825{
4826	global $dbxlink;
4827	switch ($type)
4828	{
4829	case 'image/x-png':
4830		$type = 'image/png';
4831		break;
4832	case 'image/pjpeg':
4833		$type = 'image/jpeg';
4834		break;
4835	default:
4836	}
4837	try
4838	{
4839		# File.size has no default value, set to 0 with MySQL strict mode in mind.
4840		$query = $dbxlink->prepare ('INSERT INTO File (name, type, size, ctime, mtime, atime, contents, comment) VALUES (?, ?, 0, NOW(), NOW(), NOW(), ?, ?)');
4841		$query->bindParam (1, $name);
4842		$query->bindParam (2, $type);
4843		$query->bindParam (3, $contents, PDO::PARAM_LOB);
4844		$query->bindParam (4, $comment);
4845		$query->execute();
4846		$file_id = lastInsertID();
4847		usePreparedExecuteBlade ('UPDATE File SET size = LENGTH(contents) WHERE id = ?', array ($file_id));
4848		return $file_id;
4849	}
4850	catch (PDOException $e)
4851	{
4852		throw convertPDOException ($e);
4853	}
4854}
4855
4856function commitReplaceFile ($file_id = 0, $contents)
4857{
4858	global $dbxlink;
4859	$query = $dbxlink->prepare('UPDATE File SET mtime = NOW(), contents = ?, size = LENGTH(contents), thumbnail = NULL WHERE id = ?');
4860	$query->bindParam(1, $contents, PDO::PARAM_LOB);
4861	$query->bindParam(2, $file_id);
4862
4863	try
4864	{
4865		return $query->execute();
4866	}
4867	catch (PDOException $e)
4868	{
4869		throw convertPDOException ($e);
4870	}
4871}
4872
4873function commitUnlinkFile ($link_id)
4874{
4875	usePreparedDeleteBlade ('FileLink', array ('id' => $link_id));
4876}
4877
4878function commitDeleteFile ($file_id)
4879{
4880	destroyTagsForEntity ('file', $file_id);
4881	usePreparedDeleteBlade ('File', array ('id' => $file_id));
4882}
4883
4884function getChapterList ()
4885{
4886	$result = usePreparedSelectBlade
4887	(
4888		'SELECT id, sticky, name, count(chapter_id) as wordc ' .
4889		'FROM Chapter LEFT JOIN Dictionary ON Chapter.id = chapter_id ' .
4890		'GROUP BY id ORDER BY name'
4891	);
4892	return reindexById ($result->fetchAll (PDO::FETCH_ASSOC));
4893}
4894
4895// Return file id by file name.
4896function findFileByName ($filename)
4897{
4898	$result = usePreparedSelectBlade ('SELECT id FROM File WHERE name = ?', array ($filename));
4899	return nullIfFalse ($result->fetchColumn());
4900}
4901
4902function fetchLDAPCacheRow ($username, $extrasql = '')
4903{
4904	$result = usePreparedSelectBlade
4905	(
4906		'SELECT TIMESTAMPDIFF(SECOND, first_success, now()) AS success_age, ' .
4907		'TIMESTAMPDIFF(SECOND, last_retry, now()) AS retry_age, displayed_name, memberof, successful_hash ' .
4908		'FROM LDAPCache WHERE presented_username = ? ' . $extrasql,
4909		array ($username)
4910	);
4911	$row = $result->fetch (PDO::FETCH_ASSOC);
4912	if ($row)
4913	{
4914		$members = unserialize (base64_decode ($row['memberof']));
4915		$row['memberof'] = is_array ($members) ? $members : array();
4916	}
4917	return $row;
4918}
4919
4920// locks LDAPCache row for given username or throws an exception
4921function acquireLDAPCache ($username, $max_tries = 2)
4922{
4923	$self = __FUNCTION__;
4924	global $dbxlink;
4925
4926	// guarantee there is a row to lock
4927	usePreparedExecuteBlade ("INSERT IGNORE INTO LDAPCache (presented_username) VALUES (?)", array ($username));
4928	$dbxlink->beginTransaction();
4929
4930	if ($row = fetchLDAPCacheRow ($username, 'FOR UPDATE'))
4931		return $row;
4932
4933	// Maybe another instance deleted the row before this instance had locked it. Try again.
4934	if ($max_tries > 0)
4935		return $self ($username, $max_tries - 1);
4936
4937	// the problem still persists after retries, throw an exception
4938	throw new RackTablesError ("Unable to acquire lock on LDAPCache", RackTablesError::INTERNAL);
4939}
4940
4941function releaseLDAPCache ()
4942{
4943	global $dbxlink;
4944	$dbxlink->commit();
4945}
4946
4947// This actually changes only last_retry.
4948function touchLDAPCacheRecord ($username)
4949{
4950	usePreparedExecuteBlade ('UPDATE LDAPCache SET last_retry=NOW() WHERE presented_username=?', array ($username));
4951}
4952
4953function replaceLDAPCacheRecord ($username, $password_hash, $dname, $memberof)
4954{
4955	usePreparedDeleteBlade ('LDAPCache', array ('presented_username' => $username));
4956	usePreparedInsertBlade ('LDAPCache',
4957		array
4958		(
4959			'presented_username' => $username,
4960			'successful_hash' => $password_hash,
4961			'displayed_name' => $dname,
4962			'memberof' => base64_encode (serialize ($memberof)),
4963		)
4964	);
4965}
4966
4967function deleteLDAPCacheRecord ($username)
4968{
4969	usePreparedDeleteBlade ('LDAPCache', array ('presented_username' => $username));
4970}
4971
4972// Purge all records older than the threshold, as well as any records made in future.
4973// Calling this function w/o argument purges the whole LDAP cache.
4974function discardLDAPCache ($maxseconds = 0)
4975{
4976	usePreparedExecuteBlade ('DELETE FROM LDAPCache WHERE TIMESTAMPDIFF(SECOND, first_success, NOW()) >= ? OR NOW() < first_success', array ($maxseconds));
4977}
4978
4979function getUserIDByUsername ($username)
4980{
4981	$result = usePreparedSelectBlade ('SELECT user_id FROM UserAccount WHERE user_name = ?', array ($username));
4982	return nullIfFalse ($result->fetchColumn());
4983}
4984
4985# Derive a complete cell structure from the given username regardless
4986# if it is a local account or not.
4987function constructUserCell ($username)
4988{
4989	if (NULL !== ($userid = getUserIDByUsername ($username)))
4990		return spotEntity ('user', $userid);
4991	$ret = array
4992	(
4993		'realm' => 'user',
4994		'user_name' => $username,
4995		'user_realname' => '',
4996		'etags' => array(),
4997		'itags' => array(),
4998	);
4999	$ret['atags'] = generateEntityAutoTags ($ret);
5000	return $ret;
5001}
5002
5003// DEPRECATED but snmpgeneric.php uses it, remove in 0.21.0.
5004function alreadyUsedL2Address ($address, $my_object_id)
5005{
5006	try
5007	{
5008		assertUniqueL2Addresses (array ($address), $my_object_id);
5009		return FALSE;
5010	}
5011	catch (InvalidArgException $iae)
5012	{
5013		return TRUE;
5014	}
5015}
5016
5017// Raise an exception if any of the given MAC/WWN addresses (less empty strings)
5018// belongs to a port with an object ID other than the given. This constraint makes
5019// it possible to reuse L2 addresses within one object's set of ports and to keep
5020// them universally unique otherwise. Every L2 address on the input list must have
5021// been conditioned with l2addressForDatabase().
5022function assertUniqueL2Addresses ($db_l2addresses, $my_object_id)
5023{
5024	// Reindex the array such that array_merge() below works as expected.
5025	$db_l2addresses = array_values (array_unique (array_filter ($db_l2addresses, 'strlen')));
5026	if (0 == count ($db_l2addresses))
5027		return;
5028	$qm = questionMarks (count ($db_l2addresses));
5029	// BINARY in the second comparison is what the query is actually looking for but without
5030	// the first (non-BINARY) comparison the table index does not work as expected.
5031	$query = 'SELECT l2address, object_id, name FROM Port ' .
5032		"WHERE l2address IN(${qm}) AND BINARY l2address IN(${qm}) AND object_id != ? LIMIT 1";
5033	$params = array_merge ($db_l2addresses, $db_l2addresses, array ($my_object_id));
5034	$result = usePreparedSelectBlade ($query, $params);
5035	if ($row = $result->fetch (PDO::FETCH_ASSOC))
5036		throw new InvalidArgException ('L2 address', $row['l2address'], "already used by object#{$row['object_id']} port '{$row['name']}'");
5037}
5038
5039function getPortInterfaceCompat()
5040{
5041	$result = usePreparedSelectBlade
5042	(
5043		'SELECT iif_id, iif_name, oif_id, oif_name ' .
5044		'FROM PortInterfaceCompat INNER JOIN PortInnerInterface AS PII ON PII.id = iif_id ' .
5045		'INNER JOIN PortOuterInterface AS POI ON POI.id = oif_id ' .
5046		'ORDER BY iif_name, oif_name'
5047	);
5048	return $result->fetchAll (PDO::FETCH_ASSOC);
5049}
5050
5051// Return a set of options for a plain SELECT. These options include the current
5052// OIF of the given port and all OIFs of its permanent IIF.
5053// If given port is already linked, returns only types compatible with the remote port's type
5054function getExistingPortTypeOptions ($portinfo)
5055{
5056	$remote_type = NULL;
5057	if ($portinfo['linked'])
5058	{
5059		$remote_portinfo = getPortInfo ($portinfo['remote_id']);
5060		$result = usePreparedSelectBlade ("
5061SELECT oif_id, oif_name
5062FROM PortInterfaceCompat
5063INNER JOIN PortOuterInterface ON oif_id = id
5064INNER JOIN PortCompat pc ON pc.type1 = oif_id AND pc.type2 = ?
5065WHERE iif_id = ?
5066ORDER BY oif_name
5067", array ($remote_portinfo['oif_id'], $portinfo['iif_id'])
5068		);
5069	}
5070	else
5071	{
5072		$result = usePreparedSelectBlade ("
5073SELECT oif_id, oif_name
5074FROM PortInterfaceCompat
5075INNER JOIN PortOuterInterface ON oif_id = id
5076WHERE iif_id = ?
5077ORDER BY oif_name
5078", array ($portinfo['iif_id'])
5079		);
5080	}
5081
5082	return reduceSubarraysToColumn (reindexById ($result->fetchAll (PDO::FETCH_ASSOC), 'oif_id'), 'oif_name');
5083}
5084
5085function getPortTypeUsageStatistics()
5086{
5087	$result = usePreparedSelectBlade
5088	(
5089		'SELECT p.iif_id, p.type, COUNT(p.id) AS count FROM Port p INNER JOIN Link l '.
5090		'ON (p.id = l.porta or p.id = l.portb) WHERE p.type <> 0 GROUP BY iif_id, type'
5091	);
5092	$ret = array();
5093	while ($row = $result->fetch (PDO::FETCH_ASSOC))
5094		$ret[$row['iif_id'] . '-' . $row['type']] = $row['count'];
5095	return $ret;
5096}
5097
5098function getPortIIFOptions()
5099{
5100	$result = usePreparedSelectBlade ('SELECT id, iif_name FROM PortInnerInterface ORDER BY iif_name');
5101	return reduceSubarraysToColumn (reindexById ($result->fetchAll (PDO::FETCH_ASSOC)), 'iif_name');
5102}
5103
5104function getPortOIFOptions()
5105{
5106	$result = usePreparedSelectBlade ('SELECT id, oif_name FROM PortOuterInterface ORDER BY oif_name');
5107	return reduceSubarraysToColumn (reindexById ($result->fetchAll (PDO::FETCH_ASSOC)), 'oif_name');
5108}
5109
5110function commitSupplementPIC ($iif_id, $oif_id)
5111{
5112	usePreparedInsertBlade
5113	(
5114		'PortInterfaceCompat',
5115		array ('iif_id' => $iif_id, 'oif_id' => $oif_id)
5116	);
5117}
5118
5119function getPortIIFStats ($iif_id)
5120{
5121	$result = usePreparedSelectBlade
5122	(
5123		'SELECT oif_name AS title, COUNT(Port.id) AS max, ' .
5124		'COUNT(reservation_comment) + ' .
5125		'COUNT(la.porta) + COUNT(lb.portb) AS current ' .
5126		'FROM Port INNER JOIN PortOuterInterface AS POI ON type = POI.id ' .
5127		'LEFT JOIN Link AS la ON la.porta = Port.id ' .
5128		'LEFT JOIN Link AS lb ON lb.portb = Port.id ' .
5129		'WHERE iif_id = ? GROUP BY type',
5130		array ($iif_id)
5131	);
5132	return $result->fetchAll (PDO::FETCH_ASSOC);
5133}
5134
5135function getPortInfo ($port_id)
5136{
5137	$result = fetchPortList ('Port.id = ?', array ($port_id));
5138	return count ($result) ? $result[0] : NULL;
5139}
5140
5141function getVLANDomainStats ()
5142{
5143	$result = usePreparedSelectBlade
5144	(
5145		'SELECT id, group_id, description, ' .
5146		'(SELECT COUNT(vd.id) FROM VLANDomain vd WHERE vd.group_id = VLANDomain.id) as subdomc, ' .
5147		'(SELECT COUNT(vlan_id) FROM VLANDescription WHERE domain_id = id) AS vlanc, ' .
5148		'(SELECT COUNT(ipv4net_id) FROM VLANIPv4 WHERE domain_id = id) AS ipv4netc, ' .
5149		'(SELECT COUNT(object_id) FROM VLANSwitch WHERE domain_id = id) AS switchc, ' .
5150		'(SELECT COUNT(port_name) FROM VLANSwitch AS VS INNER JOIN PortVLANMode AS PVM ON VS.object_id = PVM.object_id WHERE domain_id = id) AS portc ' .
5151		'FROM VLANDomain ORDER BY description'
5152	);
5153	$ret = reindexById ($result->fetchAll (PDO::FETCH_ASSOC));
5154	foreach ($ret as $vdom_id => $domain)
5155		if ($domain['group_id'])
5156		{
5157			// sum only vlans/nets because subdomains have switches/ports of their own
5158			$ret[$domain['group_id']]['vlanc'] += $domain['vlanc'];
5159			$ret[$domain['group_id']]['ipv4netc'] += $domain['ipv4netc'];
5160		}
5161	return $ret;
5162}
5163
5164function getVLANDomainOptions()
5165{
5166	$result = usePreparedSelectBlade ('SELECT id, description FROM VLANDomain ORDER BY description');
5167	return reduceSubarraysToColumn (reindexById ($result->fetchAll (PDO::FETCH_ASSOC)), 'description');
5168}
5169
5170function getVLANDomain ($vdid)
5171{
5172	$result = usePreparedSelectBlade
5173	(
5174		'SELECT id, group_id, description, ' .
5175		'(SELECT COUNT(vd.id) FROM VLANDomain vd WHERE vd.group_id = VLANDomain.id) as subdomc ' .
5176		'FROM VLANDomain WHERE id = ?',
5177		array ($vdid)
5178	);
5179	if (!$ret = $result->fetch (PDO::FETCH_ASSOC))
5180		throw new EntityNotFoundException ('VLAN domain', $vdid);
5181	unset ($result);
5182	$ret['vlanlist'] = getDomainVLANList ($vdid);
5183	$ret['switchlist'] = array();
5184	$result = usePreparedSelectBlade
5185	(
5186		'SELECT object_id, template_id, last_errno, out_of_sync, ' .
5187		'TIMESTAMPDIFF(SECOND, last_change, NOW()) AS age_seconds ' .
5188		'FROM VLANSwitch WHERE domain_id = ? ORDER BY object_id',
5189		array ($vdid)
5190	);
5191	while ($row = $result->fetch (PDO::FETCH_ASSOC))
5192		$ret['switchlist'][$row['object_id']] = $row;
5193	return $ret;
5194}
5195
5196function getDomainGroupMembers ($vdom_group_id)
5197{
5198	$result = usePreparedSelectBlade ("SELECT id FROM VLANDomain WHERE group_id = ?", array ($vdom_group_id));
5199	return $result->fetchAll (PDO::FETCH_COLUMN, 0);
5200}
5201
5202// This function is pretty heavy. Consider use of getDomainVLANList instead
5203// If $strict is FALSE, returns VLANs belonging to the domain or group.
5204// Otherwise the vlans of group subdomains are not returned.
5205function getDomainVLANs ($vdom_id, $strict = FALSE)
5206{
5207	$self = __FUNCTION__;
5208	if (! $strict && $members = getDomainGroupMembers ($vdom_id))
5209	{
5210		$ret = $self ($vdom_id, TRUE);
5211		foreach ($members as $member_vdom_id)
5212			foreach ($self ($member_vdom_id, TRUE) as $vid => $vlan_info)
5213				if (! isset ($ret[$vid]))
5214					$ret[$vid] = $vlan_info;
5215				else
5216				{
5217					$ret[$vid]['netc'] += $vlan_info['netc'];
5218					$ret[$vid]['portc'] += $vlan_info['portc'];
5219				}
5220		ksort ($ret, SORT_NUMERIC);
5221		return $ret;
5222	}
5223
5224	$result = usePreparedSelectBlade
5225	(<<<'END'
5226SELECT
5227	vlan_id,
5228	vlan_type,
5229	vlan_descr,
5230	(SELECT COUNT(ipv4net_id) FROM VLANIPv4 AS VI WHERE VI.domain_id = VD.domain_id and VI.vlan_id = VD.vlan_id) +
5231	(SELECT COUNT(ipv6net_id) FROM VLANIPv6 AS VI WHERE VI.domain_id = VD.domain_id and VI.vlan_id = VD.vlan_id) AS netc,
5232	s2.portc
5233FROM
5234	VLANDescription VD LEFT JOIN
5235	(
5236		SELECT
5237			PAV.vlan_id as vid,
5238			COUNT(PAV.port_name) as portc
5239		FROM
5240			VLANSwitch VS
5241			INNER JOIN VLANDescription USING (domain_id)
5242			INNER JOIN PortAllowedVLAN PAV ON PAV.object_id = VS.object_id AND VLANDescription.vlan_id = PAV.vlan_id
5243		WHERE VS.domain_id = ?
5244		GROUP BY PAV.vlan_id
5245	) AS s2 ON vlan_id = s2.vid
5246WHERE domain_id = ?
5247ORDER BY vlan_id
5248
5249END
5250		, array ($vdom_id, $vdom_id)
5251	);
5252	return reindexById ($result->fetchAll (PDO::FETCH_ASSOC), 'vlan_id');
5253}
5254
5255// faster than getDomainVLANs, but w/o statistics.
5256// If $strict is FALSE, returns VLANs belonging to the domain or group.
5257// Otherwise the vlans of group subdomains are not returned.
5258function getDomainVLANList ($vdom_id, $strict = FALSE)
5259{
5260	if (! $strict && $members = getDomainGroupMembers ($vdom_id))
5261	{
5262		$self = __FUNCTION__;
5263		$ret = $self ($vdom_id, TRUE);
5264		foreach ($members as $member_vdom_id)
5265			$ret += $self ($member_vdom_id, TRUE);
5266		return $ret;
5267	}
5268
5269	$result = usePreparedSelectBlade
5270	(<<<'END'
5271SELECT
5272	vlan_id,
5273	vlan_type,
5274	vlan_descr
5275FROM
5276	VLANDescription AS VD
5277WHERE domain_id = ?
5278ORDER BY vlan_id
5279END
5280		, array ($vdom_id)
5281	);
5282	return reindexById ($result->fetchAll (PDO::FETCH_ASSOC), 'vlan_id');
5283}
5284
5285function getVLANSwitches()
5286{
5287	$result = usePreparedSelectBlade ('SELECT object_id FROM VLANSwitch');
5288	return reduceSubarraysToColumn ($result->fetchAll (PDO::FETCH_ASSOC), 'object_id');
5289}
5290
5291function getVLANSwitchInfo ($object_id, $extrasql = '')
5292{
5293	return array_first (getVLANSwitchInfoRows (array ('object_id' => $object_id), $extrasql));
5294}
5295
5296function getVLANSwitchInfoRows ($filter = array(), $extrasql = '')
5297{
5298	$query =
5299		'SELECT object_id, domain_id, template_id, mutex_rev, out_of_sync, last_errno, ' .
5300		'UNIX_TIMESTAMP(last_change) as last_change, ' .
5301		'UNIX_TIMESTAMP(last_push_started) as last_push_started, ' .
5302		'UNIX_TIMESTAMP(last_push_finished) as last_push_finished, ' .
5303		'UNIX_TIMESTAMP(last_error_ts) as last_error_ts ' .
5304		'FROM VLANSwitch';
5305	$params = array();
5306	if ($filter)
5307		$query .= ' WHERE ' . makeWhereSQL ($filter, 'AND', $params);
5308	$query .= ' ' . $extrasql;
5309	$result = usePreparedSelectBlade ($query, $params);
5310	return $result->fetchAll (PDO::FETCH_ASSOC);
5311}
5312
5313// Reads the per-port VLAN configuration for a given object.
5314// $instance could be either 'desired' or 'cached'.
5315// $port_names is a filter enumerating names of ports to select. If empty, selects all ports.
5316function getStored8021QConfig ($object_id, $instance = 'desired', $port_names = array())
5317{
5318	global $tablemap_8021q;
5319	if (!array_key_exists ($instance, $tablemap_8021q))
5320		throw new InvalidArgException ('instance', $instance);
5321	$sql_filter = ' WHERE object_id = ?';
5322	$sql_params = array ($object_id);
5323	if (count ($port_names) == 1)
5324	{
5325		$sql_filter .= ' AND port_name = ?';
5326		$sql_params[] = array_first ($port_names);
5327	}
5328	else if (count ($port_names) > 1)
5329	{
5330		$sql_filter .= ' AND port_name IN(' . questionMarks (count ($port_names)) . ')';
5331		$sql_params = array_merge ($sql_params, $port_names);
5332	}
5333
5334	$ret = array();
5335	$result = usePreparedSelectBlade
5336	(
5337		'SELECT port_name, vlan_mode FROM ' . $tablemap_8021q[$instance]['pvm'] . $sql_filter,
5338		$sql_params
5339	);
5340	while ($row = $result->fetch (PDO::FETCH_ASSOC))
5341		$ret[$row['port_name']] = array
5342		(
5343			'mode' => $row['vlan_mode'],
5344			'allowed' => array(),
5345			'native' => 0,
5346		);
5347	unset ($result);
5348	$result = usePreparedSelectBlade
5349	(
5350		'SELECT port_name, vlan_id FROM ' . $tablemap_8021q[$instance]['pav'] . $sql_filter,
5351		$sql_params
5352	);
5353	while ($row = $result->fetch (PDO::FETCH_ASSOC))
5354		$ret[$row['port_name']]['allowed'][] = $row['vlan_id'];
5355	unset ($result);
5356	$result = usePreparedSelectBlade
5357	(
5358		'SELECT port_name, vlan_id FROM ' . $tablemap_8021q[$instance]['pnv'] . $sql_filter,
5359		$sql_params
5360	);
5361	while ($row = $result->fetch (PDO::FETCH_ASSOC))
5362		$ret[$row['port_name']]['native'] = $row['vlan_id'];
5363	return $ret;
5364}
5365
5366function getVlanRow ($vlan_ck)
5367{
5368	list ($vdom_id, $vlan_id) = decodeVLANCK ($vlan_ck);
5369	$query = 'SELECT domain_id, vlan_id, vlan_type AS vlan_prop, vlan_descr, ' .
5370		'(SELECT description FROM VLANDomain WHERE id = domain_id) AS domain_descr, ' .
5371		'(SELECT group_id FROM VLANDomain WHERE id = domain_id) AS domain_group_id ' .
5372		'FROM VLANDescription WHERE domain_id = ? AND vlan_id = ?';
5373	$result = usePreparedSelectBlade ($query, array ($vdom_id, $vlan_id));
5374	if (FALSE === $ret = $result->fetch (PDO::FETCH_ASSOC))
5375		throw new EntityNotFoundException ('VLAN', $vlan_ck);
5376	$ret['vlan_ck'] = $vlan_ck;
5377	return $ret;
5378}
5379
5380function getVLANInfo ($vlan_ck)
5381{
5382	list ($vdom_id, $vlan_id) = decodeVLANCK ($vlan_ck);
5383	$ret = getVlanRow ($vlan_ck);
5384
5385	$result = usePreparedSelectBlade
5386	(
5387	 	'SELECT ipv4net_id FROM VLANIPv4 WHERE domain_id = ? AND vlan_id = ? ORDER BY ipv4net_id',
5388		array ($vdom_id, $vlan_id)
5389	);
5390	$ret['ipv4nets'] = reduceSubarraysToColumn ($result->fetchAll (PDO::FETCH_ASSOC), 'ipv4net_id');
5391	unset ($result);
5392	$result = usePreparedSelectBlade
5393	(
5394	 	'SELECT ipv6net_id FROM VLANIPv6 WHERE domain_id = ? AND vlan_id = ? ORDER BY ipv6net_id',
5395		array ($vdom_id, $vlan_id)
5396	);
5397	$ret['ipv6nets'] = reduceSubarraysToColumn ($result->fetchAll (PDO::FETCH_ASSOC), 'ipv6net_id');
5398	return $ret;
5399}
5400
5401// return list of network IDs that are not bound to the given VLAN domain
5402function getVLANIPv4Options ($except_vdid)
5403{
5404	$prepared = usePreparedSelectBlade
5405	(
5406		'SELECT id FROM IPv4Network WHERE id NOT IN ' .
5407		'(SELECT ipv4net_id FROM VLANIPv4 WHERE domain_id = ?)' .
5408		'ORDER BY ip, mask',
5409		array ($except_vdid)
5410	);
5411	return reduceSubarraysToColumn ($prepared->fetchAll (PDO::FETCH_ASSOC), 'id');
5412}
5413
5414// return list of network IDs that are not bound to the given VLAN domain
5415function getVLANIPv6Options ($except_vdid)
5416{
5417	$prepared = usePreparedSelectBlade
5418	(
5419		'SELECT id FROM IPv6Network WHERE id NOT IN ' .
5420		'(SELECT ipv6net_id FROM VLANIPv6 WHERE domain_id = ?)' .
5421		'ORDER BY ip, mask',
5422		array ($except_vdid)
5423	);
5424	return reduceSubarraysToColumn ($prepared->fetchAll (PDO::FETCH_ASSOC), 'id');
5425}
5426
5427function commitSupplementVLANIPv4 ($vlan_ck, $ipv4net_id)
5428{
5429	list ($vdom_id, $vlan_id) = decodeVLANCK ($vlan_ck);
5430	usePreparedInsertBlade
5431	(
5432		'VLANIPv4',
5433		array
5434		(
5435			'domain_id' => $vdom_id,
5436			'vlan_id' => $vlan_id,
5437			'ipv4net_id' => $ipv4net_id,
5438		)
5439	);
5440}
5441
5442function commitSupplementVLANIPv6 ($vlan_ck, $ipv6net_id)
5443{
5444	list ($vdom_id, $vlan_id) = decodeVLANCK ($vlan_ck);
5445	usePreparedInsertBlade
5446	(
5447		'VLANIPv6',
5448		array
5449		(
5450			'domain_id' => $vdom_id,
5451			'vlan_id' => $vlan_id,
5452			'ipv6net_id' => $ipv6net_id,
5453		)
5454	);
5455}
5456
5457function commitReduceVLANIPv4 ($vlan_ck, $ipv4net_id)
5458{
5459	list ($vdom_id, $vlan_id) = decodeVLANCK ($vlan_ck);
5460	usePreparedDeleteBlade
5461	(
5462		'VLANIPv4',
5463		array
5464		(
5465			'domain_id' => $vdom_id,
5466			'vlan_id' => $vlan_id,
5467			'ipv4net_id' => $ipv4net_id,
5468		)
5469	);
5470}
5471
5472function commitReduceVLANIPv6 ($vlan_ck, $ipv6net_id)
5473{
5474	list ($vdom_id, $vlan_id) = decodeVLANCK ($vlan_ck);
5475	usePreparedDeleteBlade
5476	(
5477		'VLANIPv6',
5478		array
5479		(
5480			'domain_id' => $vdom_id,
5481			'vlan_id' => $vlan_id,
5482			'ipv6net_id' => $ipv6net_id,
5483		)
5484	);
5485}
5486
5487// Return a list of switches that have specific VLAN configured on
5488// any port (each switch with the list of such ports).
5489function getVLANConfiguredPorts ($vlan_ck)
5490{
5491	$result = usePreparedSelectBlade
5492	(
5493		'SELECT PAV.object_id, PAV.port_name ' .
5494		'FROM PortAllowedVLAN AS PAV ' .
5495		'INNER JOIN VLANSwitch AS VS ON PAV.object_id = VS.object_id ' .
5496		'WHERE domain_id = ? AND vlan_id = ? ' .
5497		'ORDER BY PAV.object_id, PAV.port_name',
5498		decodeVLANCK ($vlan_ck)
5499	);
5500	$ret = array();
5501	while ($row = $result->fetch (PDO::FETCH_ASSOC))
5502		$ret[$row['object_id']][] = $row['port_name'];
5503	return $ret;
5504}
5505
5506function add8021QPort ($object_id, $port_name, $port)
5507{
5508	global $tablemap_8021q;
5509	$changed = 0;
5510	$changed += usePreparedInsertBlade
5511	(
5512		$tablemap_8021q['cached']['pvm'],
5513		array ('object_id' => $object_id, 'port_name' => $port_name, 'vlan_mode' => $port['mode'])
5514	);
5515	$changed += usePreparedInsertBlade
5516	(
5517		$tablemap_8021q['desired']['pvm'],
5518		array ('object_id' => $object_id, 'port_name' => $port_name, 'vlan_mode' => $port['mode'])
5519	);
5520	$changed += upd8021QPort ('cached', $object_id, $port_name, $port, NULL);
5521	$changed += upd8021QPort ('desired', $object_id, $port_name, $port, NULL);
5522	return $changed ? 1 : 0;
5523}
5524
5525function del8021QPort ($object_id, $port_name)
5526{
5527	// rely on ON DELETE CASCADE for PortAllowedVLAN and PortNativeVLAN
5528	global $tablemap_8021q;
5529	$changed = 0;
5530	$changed += usePreparedDeleteBlade
5531	(
5532		$tablemap_8021q['desired']['pvm'],
5533		array ('object_id' => $object_id, 'port_name' => $port_name)
5534	);
5535	$changed += usePreparedDeleteBlade
5536	(
5537		$tablemap_8021q['cached']['pvm'],
5538		array ('object_id' => $object_id, 'port_name' => $port_name)
5539	);
5540
5541	callHook ('portConfChanged', $object_id, $port_name, NULL);
5542	return $changed ? 1 : 0;
5543}
5544
5545// Returns list of tuples ("where_text" params_array) that covers
5546// all the VLANs in $vlan_list.
5547// aggregates sparse VLANs into single IN() condition
5548// each range larger than 5 is returned as separate BETWEEN condition
5549function makeVlanListWhere ($db_field_name, $vlan_list)
5550{
5551	$ret = array();
5552	$in_list = array();
5553	foreach (listToRanges ($vlan_list) as $range)
5554	{
5555		if ($range['from'] == $range['to'])
5556			$in_list[] = $range['from'];
5557		elseif ($range['to'] - $range['from'] < 5)
5558			for ($i = $range['from']; $i <= $range['to']; ++$i)
5559				$in_list[] = $i;
5560		else
5561			$ret[] = array ("$db_field_name BETWEEN ? AND ?", array ($range['from'], $range['to']));
5562	}
5563	if ($in_list)
5564		$ret[] = array ("$db_field_name IN(" . questionMarks(count ($in_list)) . ')', $in_list);
5565	return $ret;
5566}
5567
5568function upd8021QPort ($instance = 'desired', $object_id, $port_name, $port, $before)
5569{
5570	global $tablemap_8021q;
5571	if (!array_key_exists ($instance, $tablemap_8021q))
5572		throw new InvalidArgException ('instance', $instance);
5573	// Replace current port configuration with the provided one. If the new
5574	// native VLAN ID doesn't belong to the allowed list, don't issue
5575	// INSERT query, which would always trigger an FK exception.
5576	// This function indicates an error, but doesn't revert it, so it is
5577	// assummed that the calling function performs necessary transaction wrapping.
5578	// A record on a port with none VLANs allowed makes no sense regardless of port mode.
5579	if ($port['mode'] != 'trunk' && ! count ($port['allowed']))
5580		return 0;
5581	$changed = 0;
5582	if (! isset ($before) || $before['mode'] != $port['mode'])
5583		$changed += usePreparedUpdateBlade
5584		(
5585			$tablemap_8021q[$instance]['pvm'],
5586			array ('vlan_mode' => $port['mode']),
5587			array ('object_id' => $object_id, 'port_name' => $port_name)
5588		);
5589
5590	if (isset ($before))
5591	{
5592		$add_list = array_diff ($port['allowed'], $before['allowed']);
5593		$del_list = array_diff ($before['allowed'], $port['allowed']);
5594		foreach (makeVlanListWhere ('vlan_id', $del_list) as $where)
5595			$changed += usePreparedExecuteBlade
5596			(
5597				'DELETE FROM ' . $tablemap_8021q[$instance]['pav'] .
5598				' WHERE object_id = ? AND port_name = ? AND ' . $where[0],
5599				array_merge (array ($object_id, $port_name), $where[1])
5600			);
5601	}
5602	else
5603	{
5604		$add_list = $port['allowed'];
5605		$changed += usePreparedDeleteBlade (
5606			$tablemap_8021q[$instance]['pav'],
5607			array ('object_id' => $object_id, 'port_name' => $port_name)
5608		);
5609	}
5610
5611	// The goal is to INSERT as many rows as there are values in 'allowed' list
5612	// without wrapping each row with own INSERT (otherwise the SQL connection
5613	// instantly becomes the bottleneck).
5614	foreach (makeVlanListWhere ('vlan_id', $add_list) as $where)
5615		$changed += usePreparedExecuteBlade
5616		(
5617			'INSERT INTO ' . $tablemap_8021q[$instance]['pav'] . ' (object_id, port_name, vlan_id) ' .
5618			'SELECT ?, ?, vlan_id FROM VLANValidID WHERE ' . $where[0],
5619			array_merge (array ($object_id, $port_name), $where[1])
5620		);
5621
5622	if (! $port['native'] && (! isset ($before) || in_array ($before['native'], $port['allowed'])))
5623		$changed += usePreparedDeleteBlade
5624		(
5625			$tablemap_8021q[$instance]['pnv'],
5626			array ('object_id' => $object_id, 'port_name' => $port_name)
5627		);
5628	elseif ($port['native'] && (! isset ($before) || $before['native'] != $port['native']))
5629		$changed += usePreparedExecuteBlade
5630		(
5631			'REPLACE INTO ' . $tablemap_8021q[$instance]['pnv'] .
5632			' (object_id, port_name, vlan_id) VALUES (?, ?, ?)',
5633			array ($object_id, $port_name, $port['native'])
5634		);
5635
5636	if ($instance == 'desired' && $changed)
5637		callHook ('portConfChanged', $object_id, $port_name, $port);
5638	return $changed ? 1 : 0;
5639}
5640
5641function replace8021QPorts ($instance = 'desired', $object_id, $before, $changes)
5642{
5643	$done = 0;
5644	foreach ($changes as $pn => $port)
5645		$done += upd8021QPort ($instance, $object_id, $pn, $port, array_fetch ($before, $pn, NULL));
5646	return $done;
5647}
5648
5649function commitUpdateVSTRules ($vst_id, $mutex_rev, $rules)
5650{
5651	global $dbxlink, $remote_username;
5652	$dbxlink->beginTransaction();
5653	$result = usePreparedSelectBlade
5654	(
5655		'SELECT mutex_rev, saved_by FROM VLANSwitchTemplate ' .
5656		'WHERE id = ? FOR UPDATE',
5657		array ($vst_id)
5658	);
5659	$vst = $result->fetch (PDO::FETCH_ASSOC);
5660	unset ($result);
5661	if ($vst['mutex_rev'] != $mutex_rev)
5662		throw new InvalidRequestArgException ('mutex_rev', $mutex_rev, "already saved by ${vst['saved_by']}");
5663	usePreparedDeleteBlade ('VLANSTRule', array ('vst_id' => $vst_id));
5664	foreach ($rules as $rule)
5665		usePreparedInsertBlade ('VLANSTRule', array_merge (array ('vst_id' => $vst_id), $rule));
5666	usePreparedExecuteBlade ('UPDATE VLANSwitchTemplate SET mutex_rev=mutex_rev+1, saved_by=? WHERE id=?', array ($remote_username, $vst_id));
5667	$dbxlink->commit();
5668}
5669
5670// Return entity ID, if its 'name' column equals to provided string, or NULL otherwise (nothing
5671// found or more than one row returned by query due to some odd reason).
5672function lookupEntityByString ($realm, $value, $column = 'name')
5673{
5674	global $SQLSchema;
5675	if (!isset ($SQLSchema[$realm]))
5676		throw new InvalidArgException ('realm', $realm);
5677	$SQLinfo = $SQLSchema[$realm];
5678	$query = "SELECT ${SQLinfo['keycolumn']} AS id FROM ${SQLinfo['table']} WHERE ${SQLinfo['table']}.${column}=? LIMIT 2";
5679	$result = usePreparedSelectBlade ($query, array ($value));
5680	$rows = $result->fetchAll (PDO::FETCH_ASSOC);
5681	return count ($rows) == 1 ? $rows[0]['id'] : NULL;
5682}
5683
5684// Returns an array of attribute IDs that are mapped to the specified chapter.
5685function getChapterAttributes($chapter_id)
5686{
5687	$prepared = usePreparedSelectBlade ('SELECT DISTINCT attr_id FROM AttributeMap WHERE chapter_id = ?', array ($chapter_id));
5688	$rows = $prepared->fetchAll (PDO::FETCH_COLUMN);
5689	return is_array($rows) ? $rows : array();
5690}
5691
5692function getLogRecordsForObject ($object_id)
5693{
5694	$result = usePreparedSelectBlade ('SELECT id, content, date, user FROM ObjectLog WHERE object_id = ? ORDER BY date DESC', array ($object_id));
5695	return $result->fetchAll (PDO::FETCH_ASSOC);
5696}
5697
5698function getLogRecords()
5699{
5700	$result = usePreparedSelectBlade
5701	(
5702		'SELECT OL.id AS log_id, O.objtype_id, O.name, OL.content, OL.date, OL.user, O.id AS object_id ' .
5703		'FROM ObjectLog OL LEFT JOIN Object O ON OL.object_id = O.id ' .
5704		'ORDER BY OL.date DESC'
5705	);
5706	return $result->fetchAll (PDO::FETCH_ASSOC);
5707}
5708
5709// plugins may install their own settings
5710function addConfigVar ($varname, $varvalue, $vartype, $emptyok, $is_hidden, $is_userdefined, $description)
5711{
5712	usePreparedInsertBlade
5713	(
5714		'Config',
5715		array
5716		(
5717			'varname' => $varname,
5718			'varvalue' => $varvalue,
5719			'vartype' => $vartype,
5720			'emptyok' => $emptyok,
5721			'is_hidden' => $is_hidden,
5722			'is_userdefined' => $is_userdefined,
5723			'description' => $description
5724		)
5725	);
5726	global $configCache;
5727	$configCache = loadConfigDefaults ();
5728}
5729
5730// used when uninstalling a plugin
5731function deleteConfigVar ($varname)
5732{
5733	global $configCache;
5734	if (! isset ($configCache))
5735		throw new RackTablesError ('configuration cache is unavailable', RackTablesError::INTERNAL);
5736	unset ($configCache[$varname]);
5737	usePreparedDeleteBlade ('Config', array ('varname' => $varname));
5738}
5739
5740function setConfigVar ($varname, $varvalue)
5741{
5742	global $configCache;
5743	if (!isset ($configCache))
5744		throw new RackTablesError ('configuration cache is unavailable', RackTablesError::INTERNAL);
5745	if (NULL === $var = array_fetch ($configCache, $varname, NULL))
5746		throw new InvalidArgException ('varname', $varname, 'unknown variable');
5747	if ($var['is_hidden'] != 'no')
5748		throw new InvalidArgException ('varname', $varname, 'a hidden variable cannot be changed');
5749	if ($varvalue == '' && $var['emptyok'] != 'yes')
5750		throw new InvalidArgException ('varvalue', $varvalue, "'${varname}' must have a non-empty value");
5751	if ($varvalue != '' && $var['vartype'] == 'uint' && ! isUnsignedInteger ($varvalue))
5752		throw new InvalidArgException ('varvalue', $varvalue, "'${varname}' must be an unsigned integer");
5753	// Update cache only if the changes went into DB.
5754	usePreparedUpdateBlade ('Config', array ('varvalue' => $varvalue), array ('varname' => $varname));
5755	$configCache[$varname]['varvalue'] = $varvalue;
5756	$configCache[$varname]['defaultvalue'] = $varvalue;
5757	alterConfigWithUserPreferences();
5758}
5759
5760function setUserConfigVar ($varname, $varvalue)
5761{
5762	global $configCache;
5763	global $remote_username;
5764	if (!isset ($configCache))
5765		throw new RackTablesError ('configuration cache is unavailable', RackTablesError::INTERNAL);
5766	if (NULL === $var = array_fetch ($configCache, $varname, NULL))
5767		throw new InvalidArgException ('varname', $varname, 'unknown variable');
5768	if ($var['is_userdefined'] != 'yes')
5769		throw new InvalidArgException ('varname', $varname, 'a system-wide setting cannot be changed by user');
5770	if ($var['is_hidden'] != 'no')
5771		throw new InvalidArgException ('varname', $varname, 'a hidden variable cannot be changed');
5772	if ($varvalue == '' && $var['emptyok'] != 'yes')
5773		throw new InvalidArgException ('varvalue', $varvalue, "'${varname}' must have a non-empty value");
5774	if ($varvalue != '' && $var['vartype'] == 'uint' && ! isUnsignedInteger ($varvalue))
5775		throw new InvalidArgException ('varvalue', $varvalue, "'${varname}' must be an unsigned integer");
5776	// Update cache only if the changes went into DB.
5777	usePreparedExecuteBlade
5778	(
5779		'REPLACE INTO UserConfig SET varvalue=?, varname=?, user=?',
5780		array ($varvalue, $varname, $remote_username)
5781	);
5782	$configCache[$varname]['varvalue'] = $varvalue;
5783	$configCache[$varname]['is_altered'] = 'yes';
5784}
5785
5786function resetUserConfigVar ($varname)
5787{
5788	global $configCache;
5789	global $remote_username;
5790	if (!isset ($configCache))
5791		throw new RackTablesError ('configuration cache is unavailable', RackTablesError::INTERNAL);
5792	if (NULL === $var = array_fetch ($configCache, $varname, NULL))
5793		throw new InvalidArgException ('varname', $varname, 'unknown variable');
5794	if ($var['is_hidden'] != 'no')
5795		throw new InvalidArgException ('varname', $varname, 'a hidden variable cannot be changed');
5796	// Update cache only if the changes went into DB.
5797	if (! array_key_exists ('is_altered', $var) || $var['is_altered'] != 'yes')
5798		return;
5799	usePreparedDeleteBlade ('UserConfig', array ('varname' => $varname, 'user' => $remote_username));
5800	$configCache[$varname]['varvalue'] = $configCache[$varname]['defaultvalue'];
5801	unset ($configCache[$varname]['is_altered']);
5802}
5803
5804// parses QUICK_LINK_PAGES config var and returns array with ('href'=>..., 'title'=>...) items
5805function getConfiguredQuickLinks()
5806{
5807	$ret = array();
5808	foreach (explode (',', getConfigVar('QUICK_LINK_PAGES')) as $page_code)
5809		if ($page_code != '')
5810			if ('' != $title = getPageName ($page_code))
5811				$ret[] = array ('href' => makeHref (array ('page' => $page_code)), 'title' => $title);
5812	return $ret;
5813}
5814
5815function touchVLANSwitch ($switch_id)
5816{
5817	return usePreparedExecuteBlade
5818	(
5819		'UPDATE VLANSwitch SET mutex_rev=mutex_rev+1, last_change=NOW(), out_of_sync="yes" WHERE object_id=?',
5820		array ($switch_id)
5821	);
5822}
5823
5824function detouchVLANSwitch ($switch_id, $mutex_rev)
5825{
5826	return usePreparedExecuteBlade
5827	(
5828		'UPDATE VLANSwitch SET last_change=NOW(), out_of_sync="no" WHERE object_id=? AND mutex_rev = ?',
5829		array ($switch_id, $mutex_rev)
5830	);
5831}
5832
5833function setVLANSwitchError ($object_id, $errno)
5834{
5835	return usePreparedExecuteBlade
5836	(
5837		'UPDATE VLANSwitch SET last_errno=?, last_error_ts=NOW() WHERE object_id=?',
5838		array ($errno, $object_id)
5839	);
5840}
5841
5842function setVLANSwitchTimestamp ($object_id, $field_name)
5843{
5844	return usePreparedExecuteBlade
5845	(
5846		"UPDATE VLANSwitch SET `$field_name`=NOW() WHERE object_id=?",
5847		array ($object_id)
5848	);
5849}
5850
5851# Return list of rows for objects that have the date stored in the given
5852# attribute belonging to the given range (relative to today's date).
5853function scanAttrRelativeDays ($attr_id, $not_before_days, $not_after_days)
5854{
5855	if (getAttrType ($attr_id) != 'date')
5856		throw new InvalidArgException ('attr_id', $attr_id, 'attribute cannot store dates');
5857	$result = usePreparedSelectBlade
5858	(
5859		'SELECT uint_value, object_id FROM AttributeValue ' .
5860		'WHERE attr_id=? and FROM_UNIXTIME(uint_value) BETWEEN '.
5861		'DATE_ADD(curdate(), INTERVAL ? DAY) and DATE_ADD(curdate(), INTERVAL ? DAY)',
5862		array ($attr_id, $not_before_days, $not_after_days)
5863	);
5864	return $result->fetchAll (PDO::FETCH_ASSOC);
5865}
5866
5867function isTransactionActive()
5868{
5869	global $dbxlink;
5870	try
5871	{
5872		if ($dbxlink->beginTransaction())
5873		{
5874			$dbxlink->rollBack();
5875			return FALSE;
5876		}
5877		throw new RackTablesError ("beginTransaction() returned FALSE instead of throwing exception", RackTablesError::INTERNAL);
5878	}
5879	catch (PDOException $e)
5880	{
5881		return TRUE;
5882	}
5883}
5884
5885function getRowsCount ($table)
5886{
5887	$result = usePreparedSelectBlade ("SELECT COUNT(*) FROM `$table`");
5888	return $result->fetchColumn();
5889}
5890
5891function getEntitiesCount ($realm)
5892{
5893	global $SQLSchema;
5894	if (!isset ($SQLSchema[$realm]))
5895		throw new InvalidArgException ('realm', $realm);
5896	return getRowsCount ($SQLSchema[$realm]['table']);
5897}
5898
5899function getPatchCableConnectorList()
5900{
5901	$result = usePreparedSelectBlade
5902	(
5903		'SELECT id, origin, connector, ' .
5904		'(SELECT COUNT(*) FROM PatchCableConnectorCompat WHERE connector_id = id) AS refc ' .
5905		'FROM PatchCableConnector ORDER BY connector'
5906	);
5907	return $result->fetchAll (PDO::FETCH_ASSOC);
5908}
5909
5910function getPatchCableConnectorOptions()
5911{
5912	$ret = array();
5913	foreach (getPatchCableConnectorList() as $item)
5914		$ret[$item['id']] = $item['connector'] . ($item['origin'] == 'custom' ? ' (custom)' : '');
5915	return $ret;
5916}
5917
5918function getPatchCableTypeList()
5919{
5920	$result = usePreparedSelectBlade
5921	(
5922		'SELECT id, origin, pctype, ' .
5923		'(SELECT COUNT(*) FROM PatchCableConnectorCompat WHERE pctype_id = PatchCableType.id) + ' .
5924		'(SELECT COUNT(*) FROM PatchCableOIFCompat WHERE pctype_id = PatchCableType.id) AS refc ' .
5925		'FROM PatchCableType ORDER BY pctype'
5926	);
5927	return $result->fetchAll (PDO::FETCH_ASSOC);
5928}
5929
5930function getPatchCableTypeOptions()
5931{
5932	$ret = array();
5933	foreach (getPatchCableTypeList() as $item)
5934		$ret[$item['id']] = $item['pctype'] . ($item['origin'] == 'custom' ? ' (custom)' : '');
5935	return $ret;
5936}
5937
5938function getPatchCableHeapSummary()
5939{
5940	$result = usePreparedSelectBlade
5941	(
5942		'SELECT PCH.id, end1_conn_id, PCC1.connector AS end1_connector, pctype_id, pctype, ' .
5943		'end2_conn_id, PCC2.connector AS end2_connector, amount, length, description, ' .
5944		'COUNT(PCHL.id) AS logc FROM PatchCableHeap AS PCH ' .
5945		'INNER JOIN PatchCableType AS PCT ON PCH.pctype_id = PCT.id ' .
5946		'INNER JOIN PatchCableConnector AS PCC1 ON end1_conn_id = PCC1.id ' .
5947		'INNER JOIN PatchCableConnector AS PCC2 ON end2_conn_id = PCC2.id ' .
5948		'LEFT JOIN PatchCableHeapLog AS PCHL ON PCH.id = PCHL.heap_id ' .
5949		'GROUP BY PCH.id ' .
5950		'ORDER BY pctype, end1_connector, end2_connector, description, id '
5951	);
5952	return reindexById ($result->fetchAll (PDO::FETCH_ASSOC));
5953}
5954
5955function getPatchCableHeapOptionsForOIF ($oif_id)
5956{
5957	$result = usePreparedSelectBlade ('SELECT pctype_id FROM PatchCableOIFCompat WHERE oif_id = ?', array ($oif_id));
5958	$pctypes = reduceSubarraysToColumn ($result->fetchAll (PDO::FETCH_ASSOC), 'pctype_id');
5959	unset ($result);
5960	$ret = array();
5961	foreach (getPatchCableHeapSummary() as $item)
5962		if ($item['amount'] > 0 && in_array ($item['pctype_id'], $pctypes))
5963			$ret[$item['id']] = formatPatchCableHeapAsPlainText ($item);
5964	return $ret;
5965}
5966
5967function getPatchCableConnectorCompat()
5968{
5969	$result = usePreparedSelectBlade
5970	(
5971		'SELECT pctype_id, pctype, connector_id, connector FROM PatchCableConnectorCompat ' .
5972		'INNER JOIN PatchCableType AS PCT ON pctype_id = PCT.id ' .
5973		'INNER JOIN PatchCableConnector AS PCC ON connector_id = PCC.id ' .
5974		'ORDER BY pctype, connector'
5975	);
5976	return $result->fetchAll (PDO::FETCH_ASSOC);
5977}
5978
5979function getPatchCableOIFCompat()
5980{
5981	$result = usePreparedSelectBlade
5982	(
5983		'SELECT pctype_id, pctype, oif_id, oif_name FROM PatchCableOIFCompat ' .
5984		'INNER JOIN PatchCableType AS PCT ON pctype_id = PCT.id ' .
5985		'INNER JOIN PortOuterInterface AS POI ON oif_id = POI.id ' .
5986		'ORDER BY pctype, oif_name'
5987	);
5988	return $result->fetchAll (PDO::FETCH_ASSOC);
5989}
5990
5991function commitModifyPatchCableAmount ($heap_id, $by_amount)
5992{
5993	global $dbxlink;
5994	$dbxlink->beginTransaction();
5995	usePreparedExecuteBlade
5996	(
5997		'UPDATE PatchCableHeap SET amount = amount + ? WHERE id = ? AND amount + ? >= 0',
5998		array ($by_amount, $heap_id, $by_amount)
5999	);
6000	addPatchCableHeapLogEntry ($heap_id, "amount adjusted by ${by_amount}");
6001	return $dbxlink->commit();
6002}
6003
6004function commitSetPatchCableAmount ($heap_id, $new_amount)
6005{
6006	global $dbxlink;
6007	$dbxlink->beginTransaction();
6008	usePreparedUpdateBlade
6009	(
6010		'PatchCableHeap',
6011		array ('amount' => $new_amount),
6012		array ('id' => $heap_id)
6013	);
6014	addPatchCableHeapLogEntry ($heap_id, "amount set to ${new_amount}");
6015	return $dbxlink->commit();
6016}
6017
6018function getPatchCableHeapLogEntries ($heap_id)
6019{
6020	$result = usePreparedSelectBlade
6021	(
6022		'SELECT date, user, message FROM PatchCableHeapLog WHERE heap_id = ? ORDER BY date DESC',
6023		array ($heap_id)
6024	);
6025	return $result->fetchAll (PDO::FETCH_ASSOC);
6026}
6027
6028function addPatchCableHeapLogEntry ($heap_id, $message)
6029{
6030	global $disable_logging;
6031	if (isset ($disable_logging) && $disable_logging)
6032		return;
6033	global $remote_username;
6034	usePreparedExecuteBlade
6035	(
6036		"INSERT INTO PatchCableHeapLog (heap_id, date, user, message) VALUES (?, NOW(), ?, ?)",
6037		array ($heap_id, $remote_username, $message)
6038	);
6039}
6040
6041function selectRackOrder ($row_id)
6042{
6043	$result = usePreparedSelectBlade ("SELECT id FROM Rack WHERE row_id = ? ORDER BY sort_order, name", array($row_id));
6044	return $result->fetchAll (PDO::FETCH_COLUMN, 0);
6045}
6046
6047function getDBName()
6048{
6049	global $pdo_dsn;
6050	if (preg_match ('/\bdbname=(.+?)(;|$)/', $pdo_dsn, $m))
6051		return $m[1];
6052	throw new RackTablesError ('failed to spot "dbname" in $pdo_dsn', RackTablesError::INTERNAL);
6053}
6054
6055// Sets exclusive server-global named lock.
6056// Returns TRUE or throws an exceptions.
6057// A lock is implicitly released on any subsequent call to setDBMutex in the same connection
6058function setDBMutex ($name, $timeout = 5)
6059{
6060	$fullname = getDBName() . '.' . $name;
6061	$result = usePreparedSelectBlade ('SELECT GET_LOCK(?, ?)', array ($fullname, $timeout));
6062	$row = $result->fetchColumn();
6063	if ($row === NULL)
6064		throw new RTDatabaseError ("error occured when executing GET_LOCK on $fullname");
6065	if ($row !== '1')
6066		throw new RTDatabaseError ("lock wait timeout for $fullname");
6067	return TRUE;
6068}
6069
6070function tryDBMutex ($name, $timeout = 0)
6071{
6072	try
6073	{
6074		return setDBMutex ($name, $timeout);
6075	}
6076	catch (RTDatabaseError $e)
6077	{
6078		return FALSE;
6079	}
6080}
6081
6082function releaseDBMutex ($name)
6083{
6084	$result = usePreparedSelectBlade ('SELECT RELEASE_LOCK(?)', array (getDBName() . '.' . $name));
6085	$row = $result->fetchColumn();
6086	return $row === '1';
6087}
6088
6089// valid states: disabled, enabled, not_installed
6090// if no state is specified, return all
6091function getPlugins ($state = NULL)
6092{
6093	// installed
6094	$result = usePreparedSelectBlade ('SELECT name, longname, version, home_url, state FROM Plugin ORDER BY name');
6095	$in_db = array ();
6096	foreach ($result as $row)
6097		if (! $state or $state == $row['state'])
6098			$in_db[$row['name']] = array
6099			(
6100				'longname' => $row['longname'],
6101				'code_version' => 'N/A',
6102				'db_version' => $row['version'],
6103				'home_url' => $row['home_url'],
6104				'state' => $row['state']
6105			);
6106
6107	// available
6108	global $racktables_plugins_dir;
6109	$plugin_dirs = glob ("${racktables_plugins_dir}/*", GLOB_ONLYDIR);
6110	$in_code = array ();
6111	foreach ($plugin_dirs as $plugin_dir)
6112	{
6113		$plugin = basename ($plugin_dir);
6114		if (! file_exists ("${plugin_dir}/plugin.php"))
6115			continue;
6116
6117		require_once "${plugin_dir}/plugin.php";
6118		if (! function_exists ("plugin_${plugin}_info"))
6119			continue;
6120
6121		$info = call_user_func ("plugin_${plugin}_info");
6122		$in_code[$info['name']] = array
6123		(
6124			'longname' => $info['longname'],
6125			'code_version' => $info['version'],
6126			'db_version' => (array_key_exists ($plugin, $in_db) ? $in_db[$info['name']]['db_version'] : 'N/A'),
6127			'home_url' => $info['home_url'],
6128			'state' => (array_key_exists ($plugin, $in_db) ? $in_db[$info['name']]['state'] : 'not_installed')
6129		);
6130	}
6131
6132	// merge plugin lists and filter them, if needed
6133	$ret = array_replace_recursive ($in_db, $in_code);
6134	if ($state)
6135		foreach ($ret as $name => $info)
6136			if ($info['state'] != $state)
6137				unset ($ret[$name]);
6138	ksort ($ret);
6139	return $ret;
6140}
6141
6142function getPlugin ($name)
6143{
6144	$result = usePreparedSelectBlade
6145	(
6146		'SELECT name, longname, version, home_url, state FROM Plugin WHERE name = ?',
6147		array ($name)
6148	);
6149	$db_info = $result->fetch (PDO::FETCH_ASSOC);
6150	$in_db = array ();
6151	if (is_array ($db_info))
6152	{
6153		$in_db['name'] = $db_info['name'];
6154		$in_db['longname'] = $db_info['longname'];
6155		$in_db['code_version'] = 'N/A';
6156		$in_db['db_version'] = $db_info['version'];
6157		$in_db['home_url'] = $db_info['home_url'];
6158		$in_db['state'] = $db_info['state'];
6159	}
6160
6161	global $racktables_plugins_dir;
6162	$in_code = array ();
6163	if (file_exists ("$racktables_plugins_dir/${name}/plugin.php"))
6164	{
6165		require_once "${racktables_plugins_dir}/${name}/plugin.php";
6166		if (! function_exists ("plugin_${name}_info"))
6167			return FALSE;
6168
6169		$code_info = call_user_func ("plugin_${name}_info");
6170		$in_code['name'] = $code_info['name'];
6171		$in_code['longname'] = $code_info['longname'];
6172		$in_code['code_version'] = $code_info['version'];
6173		$in_code['db_version'] = (isset ($in_db['db_version']) ? $in_db['db_version'] : 'N/A');
6174		$in_code['home_url'] = $code_info['home_url'];
6175		$in_code['state'] = (isset ($in_db['state']) ? $in_db['state'] : 'not_installed');
6176	}
6177	return array_replace ($in_db, $in_code);
6178}
6179
6180function commitInstallPlugin ($name, $longname, $version, $home_url)
6181{
6182	usePreparedInsertBlade
6183	(
6184		'Plugin',
6185		array
6186		(
6187			'name' => $name,
6188			'longname' => $longname,
6189			'version' => $version,
6190			'home_url' => $home_url,
6191			'state' => 'enabled'
6192		)
6193	);
6194}
6195
6196function commitUninstallPlugin ($name)
6197{
6198	usePreparedDeleteBlade ('Plugin', array ('name' => $name));
6199}
6200