network-attrs: custom attributes for IP networks
[racktables-contribs] / network-attrs.php
CommitLineData
a2b02668
AA
1<?php
2
3/*
4This plugin implements assigning custom attributes to IP networks.
5To make it working, apply these changes to the SQL database:
6
7CREATE TABLE `AttributeValue_IPv4` (
8 `net_id` int(10) unsigned DEFAULT NULL,
9 `object_tid` int(10) unsigned NOT NULL DEFAULT '49000',
10 `attr_id` int(10) unsigned DEFAULT NULL,
11 `string_value` char(255) DEFAULT NULL,
12 `uint_value` int(10) unsigned DEFAULT NULL,
13 `float_value` float DEFAULT NULL,
14 UNIQUE KEY `net_id` (`net_id`,`attr_id`),
15 KEY `attr_id-uint_value` (`attr_id`,`uint_value`),
16 KEY `attr_id-string_value` (`attr_id`,`string_value`(12)),
17 KEY `id-tid` (`net_id`,`object_tid`),
18 KEY `object_tid-attr_id` (`net_id`,`attr_id`),
19 KEY `AttributeValue_IPv4-FK-map` (`object_tid`,`attr_id`),
20 CONSTRAINT `AttributeValue_IPv4-FK-map` FOREIGN KEY (`object_tid`, `attr_id`) REFERENCES `AttributeMap` (`objtype_id`, `attr_id`),
21 CONSTRAINT `AttributeValue_IPv4-FK-object` FOREIGN KEY (`net_id`) REFERENCES `IPv4Network` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
22) ENGINE=InnoDB DEFAULT CHARSET=utf8;
23
24CREATE TABLE `AttributeValue_IPv6` (
25 `net_id` int(10) unsigned DEFAULT NULL,
26 `object_tid` int(10) unsigned NOT NULL DEFAULT '49001',
27 `attr_id` int(10) unsigned DEFAULT NULL,
28 `string_value` char(255) DEFAULT NULL,
29 `uint_value` int(10) unsigned DEFAULT NULL,
30 `float_value` float DEFAULT NULL,
31 UNIQUE KEY `net_id` (`net_id`,`attr_id`),
32 KEY `attr_id-uint_value` (`attr_id`,`uint_value`),
33 KEY `attr_id-string_value` (`attr_id`,`string_value`(12)),
34 KEY `id-tid` (`net_id`,`object_tid`),
35 KEY `object_tid-attr_id` (`net_id`,`attr_id`),
36 KEY `AttributeValue_IPv6-FK-map` (`object_tid`,`attr_id`),
37 CONSTRAINT `AttributeValue_IPv6-FK-map` FOREIGN KEY (`object_tid`, `attr_id`) REFERENCES `AttributeMap` (`objtype_id`, `attr_id`),
38 CONSTRAINT `AttributeValue_IPv6-FK-object` FOREIGN KEY (`net_id`) REFERENCES `IPv6Network` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
39) ENGINE=InnoDB DEFAULT CHARSET=utf8;
40
41INSERT INTO Dictionary (dict_key, chapter_id, dict_sticky, dict_value) VALUES
42 (49000, 1, 'no', 'IPv4 network (dummy)'),
43 (49001, 1, 'no', 'IPv6 network (dummy)');
44
45*/
46
47$page['flatip']['title'] = 'IP networks';
48$page['flatip']['parent'] = 'index';
49$tabhandler['flatip']['default'] = 'renderFlatIP';
50registerHook ('modifyEntitySummary', 'addAttributesToNetworkSummary', 'chain');
51
52foreach (array ('ipv4net', 'ipv6net') as $net_realm)
53{
54 registerTabHandler ($net_realm, 'properties', 'renderNetworkEditAttrs');
55 registerOpHandler ($net_realm, 'properties', 'updateAttrs', 'handleNetworkAttrsChange');
56 registerOpHandler ($net_realm, 'properties', 'clearSticker', 'handleNetworkStickerClear');
57}
58
59// Pseudo-object type ids.
60// Network attributes are assigned to these object types.
61$netobject_type_id = array
62(
63 'ipv4net' => 49000,
64 'ipv6net' => 49001,
65);
66
67// Special page 'flatip' handler that lists networks like the 'depot' page lists object.
68// Lists both IP families on the same page.
69// No network hierarchy is displayed, that's why 'flat'.
70function renderFlatIP()
71{
72 if (isset ($_REQUEST['attr_id']) && isset ($_REQUEST['attr_value']))
73 {
74 $params = array ('attr_id' => $_REQUEST['attr_id'], 'attr_value' => $_REQUEST['attr_value']);
75 $av = $_REQUEST['attr_value'];
76 if ($av === 'NULL')
77 $av = NULL;
78 $nets = fetchNetworksByAttr ($_REQUEST['attr_id'], $av, TRUE);
79 }
80 else
81 {
82 $params = array();
83 $nets = array_merge (listCells ('ipv4net'), listCells ('ipv6net'));
84 }
85 $cf = getCellFilter();
86 $nets = filterCellList ($nets, $cf['expression']);
87 echo "<table border=0 class=objectview>\n";
88 echo "<tr><td class=pcleft>";
89 startPortlet (sprintf ("Networks (%d)", count ($nets)));
90 echo '<ol>';
91 foreach ($nets as $network)
92 {
93 echo '<li>';
94 renderCell ($network);
95 echo '</li>';
96 }
97 echo '</ol>';
98 finishPortlet();
99 echo '</td><td class=pcright>';
100 renderCellFilterPortlet ($cf, 'ipv4net', $nets, $params);
101 echo '</td></tr></table>';
102}
103
104function addAttributesToNetworkSummary ($ret, $cell, $summary)
105{
106 if (!isset ($cell['realm']) || $cell['realm'] !== 'ipv4net' && $cell['realm'] !== 'ipv6net')
107 return $ret;
108
109 foreach (getAttrValuesForNetwork ($cell) as $record)
110 if
111 (
112 strlen ($record['value']) and
113 permitted (NULL, NULL, NULL, array (array ('tag' => '$attr_' . $record['id'])))
114 )
115 {
116 if (! isset ($record['key']))
117 $value = formatAttributeValue ($record);
118 else
119 {
120 $href = makeHref
121 (
122 array
123 (
124 'page' => 'flatip',
125 'tab' => 'default',
126 'attr_id' => $record['id'],
127 'attr_value' => $record['key'],
128 'clear-cf' => ''
129 )
130 );
131 $value = '<a href="' . $href . '">' . $record['a_value'] . '</a>';
132 }
133
134 $ret['{sticker}' . $record['name']] = $value;
135 }
136 return $ret;
137}
138
139// source: renderEditObjectForm
140function renderNetworkEditAttrs()
141{
142 global $pageno, $netobject_type_id;
143 $network = spotEntity ($pageno === 'ipv4net' ? 'ipv4net' : 'ipv6net', getBypassValue());
144 $values = getAttrValuesForNetwork ($network);
145
146 echo '<p>';
147 startPortlet ("Attributes");
148 printOpFormIntro ('updateAttrs');
149 // optional attributes
150 echo '<table border=0 cellspacing=0 cellpadding=3 align=center>';
151 $suggest_records = array();
152 if (count($values) > 0)
153 {
154 $i = 0;
155 foreach ($values as $record)
156 {
157 if (! permitted (NULL, NULL, NULL, array (
158 array ('tag' => '$attr_' . $record['id']),
159 array ('tag' => '$any_op')
160 ))
161 )
162 continue;
163 echo "<input type=hidden name=${i}_attr_id value=${record['id']}>";
164 echo '<tr><td>';
165 if (strlen ($record['value']))
166 {
167 echo "<a href='".makeHrefProcess(array('op'=>'clearSticker', 'id'=>$network['id'], 'attr_id'=>$record['id']))."'>";
168 printImageHREF ('clear', 'Clear value');
169 echo '</a>';
170 }
171 else
172 echo '&nbsp;';
173 echo '</td>';
174 echo "<th class=sticker>${record['name']}:</th><td class=tdleft>";
175 switch ($record['type'])
176 {
177 case 'uint':
178 case 'float':
179 case 'string':
180 echo "<input type=text name=${i}_value value='${record['value']}'>";
181 break;
182 case 'dict':
183 $suggest_records = array();
184 $chapter = readChapter ($record['chapter_id'], 'o');
185 foreach ($chapter as $id => $value)
186 $suggest_records[$id] = array("id" => $id, "tag" => $value);
187 printSuggestedInput($i . "_value", $suggest_records, array("id" => $record['key']), "macros");
188 enableTagsPicker ();
189 break;
190 case 'date':
191 $date_value = $record['value'] ? date(getConfigVar('DATETIME_FORMAT'), $record['value']) : '';
192 echo "<input type=text name=${i}_value value='${date_value}'>";
193 break;
194 }
195 $i++;
196 echo '<input type=hidden name=num_attrs value=' . $i . ">\n";
197 }
198 }
199 echo '</table>';
200 printImageHREF ('SAVE', 'Save changes', TRUE);
201 echo '</form>';
202 finishPortlet();
203}
204
205// source: updateObject
206function handleNetworkAttrsChange()
207{
208 genericAssertion ('num_attrs', 'uint0');
209 global $dbxlink, $sic, $pageno;
210 $network = spotEntity ($pageno === 'ipv4net' ? 'ipv4net' : 'ipv6net', getBypassValue());
211
212 $dbxlink->beginTransaction();
213
214 // Update optional attributes
215 $oldvalues = getAttrValuesForNetwork ($network);
216 for ($i = 0; $i < $_REQUEST['num_attrs']; $i++)
217 {
218 genericAssertion ("${i}_attr_id", 'uint');
219 $attr_id = $_REQUEST["${i}_attr_id"];
220 if (! array_key_exists ($attr_id, $oldvalues))
221 throw new InvalidRequestArgException ('attr_id', $attr_id, 'malformed request');
222 $value = $_REQUEST["${i}_value"];
223
224 if ('date' == $oldvalues[$attr_id]['type']) {
225 assertDateArg ("${i}_value", TRUE);
226 if ($value != '')
227 $value = strtotime ($value);
228 }
229
230 # Delete attribute and move on, when the field is empty or if the field
231 # type is a dictionary and it is the "--NOT SET--" value of 0.
232 if ($value == '' || ($oldvalues[$attr_id]['type'] == 'dict' && $value == 0))
233 {
234 if (permitted (NULL, NULL, NULL, array (array ('tag' => '$attr_' . $attr_id))))
235 commitUpdateAttrForNetwork ($network, $attr_id);
236 else
237 showError ('Permission denied, "' . $oldvalues[$attr_id]['name'] . '" left unchanged');
238 continue;
239 }
240
241 // The value could be uint/float, but we don't know ATM. Let SQL
242 // server check this and complain.
243 assertStringArg ("${i}_value");
244 switch ($oldvalues[$attr_id]['type'])
245 {
246 case 'uint':
247 case 'float':
248 case 'string':
249 case 'date':
250 $oldvalue = $oldvalues[$attr_id]['value'];
251 break;
252 case 'dict':
253 $oldvalue = $oldvalues[$attr_id]['key'];
254 break;
255 default:
256 }
257 if ($value === $oldvalue) // ('' == 0), but ('' !== 0)
258 continue;
259 if (permitted (NULL, NULL, NULL, array (array ('tag' => '$attr_' . $attr_id))))
260 commitUpdateAttrForNetwork ($network, $attr_id, $value);
261 else
262 showError ('Permission denied, "' . $oldvalues[$attr_id]['name'] . '" left unchanged');
263 }
264
265 $dbxlink->commit();
266 return showSuccess ("Attributes were updated successfully");
267
268}
269
270// source: clearSticker
271function handleNetworkStickerClear()
272{
273 global $sic, $pageno;
274 assertUIntArg ('attr_id');
275 if (permitted (NULL, NULL, NULL, array (array ('tag' => '$attr_' . $sic['attr_id']))))
276 {
277 commitUpdateAttrForNetwork (spotEntity ($pageno === 'ipv4net' ? 'ipv4net' : 'ipv6net', getBypassValue()), $sic['attr_id']);
278 showSuccess ("Attribute value cleared successfully");
279 }
280 else
281 {
282 $oldvalues = getAttrValues (getBypassValue());
283 showError ('Permission denied, "' . $oldvalues[$sic['attr_id']]['name'] . '" left unchanged');
284 }
285}
286
287// returns an array of attribute values for a given network.
288// result is indexed by attr_id
289// source: fetchAttrsForObjects
290function getAttrValuesForNetwork ($network)
291{
292 global $netobject_type_id;
293
294 switch ($network['realm'])
295 {
296 case 'ipv4net':
297 $av_table = 'AttributeValue_IPv4';
298 $o_table = 'IPv4Network';
299 break;
300 case 'ipv6net':
301 $av_table = 'AttributeValue_IPv6';
302 $o_table = 'IPv6Network';
303 break;
304 default:
305 throw new InvalidArgException ('realm', $network['realm'], "Unknown realm");
306 }
307
308 $ret = array();
309 $query =
310 "select AM.attr_id, A.name as attr_name, A.type as attr_type, C.name as chapter_name, " .
311 "C.id as chapter_id, AV.uint_value, AV.float_value, AV.string_value, D.dict_value, O.id as object_id from " .
312 "$o_table as O left join AttributeMap as AM on AM.objtype_id = ? " .
313 "left join Attribute as A on AM.attr_id = A.id " .
314 "left join $av_table as AV on AV.attr_id = AM.attr_id and AV.net_id = O.id " .
315 "left join Dictionary as D on D.dict_key = AV.uint_value and AM.chapter_id = D.chapter_id " .
316 "left join Chapter as C on AM.chapter_id = C.id " .
317 " WHERE O.id = ?";
318 $query .= " order by O.ip, O.mask";
319
320 $result = usePreparedSelectBlade ($query, array ($netobject_type_id[$network['realm']] ,$network['id']));
321 while ($row = $result->fetch (PDO::FETCH_ASSOC))
322 {
323 $object_id = $row['object_id'];
324
325 # Objects with zero attributes also matter due to the LEFT JOIN. Create
326 # keys for them too to enable negative caching.
327 if ($row['attr_id'] == NULL)
328 continue;
329
330 $record = array();
331 $record['id'] = $row['attr_id'];
332 $record['name'] = $row['attr_name'];
333 $record['type'] = $row['attr_type'];
334 switch ($row['attr_type'])
335 {
336 case 'dict':
337 $record['chapter_id'] = $row['chapter_id'];
338 $record['chapter_name'] = $row['chapter_name'];
339 $record['key'] = $row['uint_value'];
340 // fall through
341 case 'uint':
342 case 'float':
343 case 'string':
344 $record['value'] = $row[$row['attr_type'] . '_value'];
345 parseWikiLink ($record);
346 break;
347 case 'date':
348 $record['value'] = $row['uint_value'];
349 break;
350 default:
351 $record['value'] = NULL;
352 break;
353 }
354 $ret[$row['attr_id']] = $record;
355 }
356 return $ret;
357}
358
359// set/update/delete attribute value
360// source: commitUpdateAttrValue
361function commitUpdateAttrForNetwork ($network, $attr_id, $value = '')
362{
363 switch ($network['realm'])
364 {
365 case 'ipv4net':
366 $av_table = 'AttributeValue_IPv4';
367 break;
368 case 'ipv6net':
369 $av_table = 'AttributeValue_IPv6';
370 break;
371 default:
372 throw new InvalidArgException ('realm', $network['realm'], "Unknown realm");
373 }
374 $key = array ('net_id' => $network['id'], 'attr_id' => $attr_id);
375
376 $result = usePreparedSelectBlade
377 (
378 "SELECT type AS attr_type, av.* FROM Attribute a " .
379 "LEFT JOIN $av_table av ON a.id = av.attr_id AND av.net_id = ?" .
380 "WHERE a.id = ?",
381 array ($network['id'], $attr_id)
382 );
383 if (! $row = $result->fetch (PDO::FETCH_ASSOC))
384 throw new InvalidArgException ('$attr_id', $attr_id, 'No such attribute #'.$attr_id);
385 $attr_type = $row['attr_type'];
386 unset ($result);
387 switch ($attr_type)
388 {
389 case 'uint':
390 case 'float':
391 case 'string':
392 $column = $attr_type . '_value';
393 break;
394 case 'dict':
395 case 'date':
396 $column = 'uint_value';
397 break;
398 default:
399 throw new InvalidArgException ('$attr_type', $attr_type, 'Unknown attribute type found in ' . $network['realm'] . ' #' . $network['id'] . ', attribute #'.$attr_id);
400 }
401 $ret = 0;
402 if (isset ($row['attr_id']))
403 {
404 // AttributeValue row present in table
405 if ($value == '')
406 $ret = usePreparedDeleteBlade ($av_table, $key);
407 else
408 $ret = usePreparedUpdateBlade ($av_table, array ($column => $value), $key);
409 }
410 elseif ($value != '')
411 $ret = usePreparedInsertBlade ($av_table, $key + array ($column => $value));
412 return $ret;
413}
414
415// returns an array of network rows with attr_value filtered by attribute key or value
416// if $attribute_value is NULL, returns rows w/o specified attriute_id set
417// if $use_key is TRUE, $attribute_value is treated as dict_key, otherwise - as dict_value or actual value
418// if dont_filter is TRUE, all network rows are fetched. Useful to fetch all the values of a given attribute.
419function fetchNetworkRowsByAttr ($attribute_id, $attribute_value, $use_key = FALSE, $dont_filter = FALSE)
420{
421 global $netobject_type_id, $SQLSchema;
422
423 // get attribute type
424 static $map;
425 if (! isset ($map))
426 $map = getAttrMap();
427 if (! array_key_exists ($attribute_id, $map))
428 throw new InvalidArgException ('attribute_id', $attribute_id, "No such attribute");
429 $attribute = $map[$attribute_id];
430
431 // get realms
432 $realms = array();
433 foreach ($attribute['application'] as $application)
434 foreach ($netobject_type_id as $realm => $type)
435 if ($application['objtype_id'] == $type)
436 $realms[] = $realm;
437
438 $join_side = ($dont_filter && $attribute_value !== NULL) ? 'INNER' : 'LEFT';
439
440 $join = '';
441 $field = '';
442 switch ($attribute['type'])
443 {
444 case 'string':
445 $field = 'AV.string_value';
446 break;
447 case 'uint':
448 $field = 'AV.uint_value';
449 break;
450 case 'float':
451 $field = 'AV.float_value';
452 break;
453 case 'date':
454 $field = 'AV.uint_value';
455 break;
456 case 'dict':
457 if ($use_key)
458 $field = 'AV.uint_value';
459 else
460 {
461 $join = 'LEFT JOIN Dictionary D ON D.dict_key = AV.uint_value';
462 $field = 'D.dict_value';
463 }
464 break;
465 default:
466 throw new RackTablesError ();
467 }
468 $subqueries = array();
469 $params = array();
470 foreach (array ('ipv4net' => 'AttributeValue_IPv4', 'ipv6net' => 'AttributeValue_IPv6') as $realm => $table)
471 if (in_array ($realm, $realms))
472 {
473 $main_table = $SQLSchema[$realm]['table'];
474 $subquery = "
475SELECT
476 MT.id as net_id,
477 MT.ip,
478 MT.mask,
479 ? as realm,
480 $field as attr_value
481FROM
482 `$main_table` MT
483 $join_side JOIN `$table` AV ON MT.id = AV.net_id AND AV.attr_id = ?
484 $join
485";
486 $params[] = $realm;
487 $params[] = $attribute_id;
488
489 if (! $dont_filter)
490 {
491 if (isset ($attribute_value))
492 {
493 $subquery .= " WHERE $field = ?";
494 $params[] = $attribute_value;
495 }
496 else
497 $subquery .= " WHERE $field IS NULL";
498 }
499 $subqueries[] = $subquery;
500 }
501 $query = implode (' UNION ', $subqueries);
502 $result = usePreparedSelectBlade ($query, $params);
503 return $result->fetchAll (PDO::FETCH_ASSOC);
504}
505
506function fetchNetworksByAttr ($attribute_id, $attribute_value, $use_key = FALSE)
507{
508 $ret = array();
509 foreach (fetchNetworkRowsByAttr ($attribute_id, $attribute_value, $use_key) as $row)
510 {
511 $net_cell = spotEntity ($row['realm'], $row['net_id']);
512 $ret[mkKey ($net_cell)] = $net_cell;
513 }
514 return $ret;
515}
516
517?>