demo: simplify demoreload.sh
[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':
a0736a0a
AA
183 $opts = array ('0' => '(none)') + readChapter ($record['chapter_id'], 'o');
184 printSelect ($opts, array ('name' => "{$i}_value"), $record['key']);
a2b02668
AA
185 break;
186 case 'date':
187 $date_value = $record['value'] ? date(getConfigVar('DATETIME_FORMAT'), $record['value']) : '';
188 echo "<input type=text name=${i}_value value='${date_value}'>";
189 break;
190 }
191 $i++;
192 echo '<input type=hidden name=num_attrs value=' . $i . ">\n";
193 }
194 }
195 echo '</table>';
196 printImageHREF ('SAVE', 'Save changes', TRUE);
197 echo '</form>';
198 finishPortlet();
199}
200
201// source: updateObject
202function handleNetworkAttrsChange()
203{
204 genericAssertion ('num_attrs', 'uint0');
205 global $dbxlink, $sic, $pageno;
206 $network = spotEntity ($pageno === 'ipv4net' ? 'ipv4net' : 'ipv6net', getBypassValue());
207
208 $dbxlink->beginTransaction();
209
210 // Update optional attributes
211 $oldvalues = getAttrValuesForNetwork ($network);
212 for ($i = 0; $i < $_REQUEST['num_attrs']; $i++)
213 {
214 genericAssertion ("${i}_attr_id", 'uint');
215 $attr_id = $_REQUEST["${i}_attr_id"];
216 if (! array_key_exists ($attr_id, $oldvalues))
217 throw new InvalidRequestArgException ('attr_id', $attr_id, 'malformed request');
218 $value = $_REQUEST["${i}_value"];
219
220 if ('date' == $oldvalues[$attr_id]['type']) {
221 assertDateArg ("${i}_value", TRUE);
222 if ($value != '')
223 $value = strtotime ($value);
224 }
225
226 # Delete attribute and move on, when the field is empty or if the field
227 # type is a dictionary and it is the "--NOT SET--" value of 0.
228 if ($value == '' || ($oldvalues[$attr_id]['type'] == 'dict' && $value == 0))
229 {
230 if (permitted (NULL, NULL, NULL, array (array ('tag' => '$attr_' . $attr_id))))
231 commitUpdateAttrForNetwork ($network, $attr_id);
232 else
233 showError ('Permission denied, "' . $oldvalues[$attr_id]['name'] . '" left unchanged');
234 continue;
235 }
236
237 // The value could be uint/float, but we don't know ATM. Let SQL
238 // server check this and complain.
239 assertStringArg ("${i}_value");
240 switch ($oldvalues[$attr_id]['type'])
241 {
242 case 'uint':
243 case 'float':
244 case 'string':
245 case 'date':
246 $oldvalue = $oldvalues[$attr_id]['value'];
247 break;
248 case 'dict':
249 $oldvalue = $oldvalues[$attr_id]['key'];
250 break;
251 default:
252 }
253 if ($value === $oldvalue) // ('' == 0), but ('' !== 0)
254 continue;
255 if (permitted (NULL, NULL, NULL, array (array ('tag' => '$attr_' . $attr_id))))
256 commitUpdateAttrForNetwork ($network, $attr_id, $value);
257 else
258 showError ('Permission denied, "' . $oldvalues[$attr_id]['name'] . '" left unchanged');
259 }
260
261 $dbxlink->commit();
262 return showSuccess ("Attributes were updated successfully");
263
264}
265
266// source: clearSticker
267function handleNetworkStickerClear()
268{
269 global $sic, $pageno;
270 assertUIntArg ('attr_id');
271 if (permitted (NULL, NULL, NULL, array (array ('tag' => '$attr_' . $sic['attr_id']))))
272 {
273 commitUpdateAttrForNetwork (spotEntity ($pageno === 'ipv4net' ? 'ipv4net' : 'ipv6net', getBypassValue()), $sic['attr_id']);
274 showSuccess ("Attribute value cleared successfully");
275 }
276 else
277 {
278 $oldvalues = getAttrValues (getBypassValue());
279 showError ('Permission denied, "' . $oldvalues[$sic['attr_id']]['name'] . '" left unchanged');
280 }
281}
282
283// returns an array of attribute values for a given network.
284// result is indexed by attr_id
285// source: fetchAttrsForObjects
286function getAttrValuesForNetwork ($network)
287{
288 global $netobject_type_id;
289
290 switch ($network['realm'])
291 {
292 case 'ipv4net':
293 $av_table = 'AttributeValue_IPv4';
294 $o_table = 'IPv4Network';
295 break;
296 case 'ipv6net':
297 $av_table = 'AttributeValue_IPv6';
298 $o_table = 'IPv6Network';
299 break;
300 default:
301 throw new InvalidArgException ('realm', $network['realm'], "Unknown realm");
302 }
303
304 $ret = array();
305 $query =
306 "select AM.attr_id, A.name as attr_name, A.type as attr_type, C.name as chapter_name, " .
307 "C.id as chapter_id, AV.uint_value, AV.float_value, AV.string_value, D.dict_value, O.id as object_id from " .
308 "$o_table as O left join AttributeMap as AM on AM.objtype_id = ? " .
309 "left join Attribute as A on AM.attr_id = A.id " .
310 "left join $av_table as AV on AV.attr_id = AM.attr_id and AV.net_id = O.id " .
311 "left join Dictionary as D on D.dict_key = AV.uint_value and AM.chapter_id = D.chapter_id " .
312 "left join Chapter as C on AM.chapter_id = C.id " .
313 " WHERE O.id = ?";
314 $query .= " order by O.ip, O.mask";
315
316 $result = usePreparedSelectBlade ($query, array ($netobject_type_id[$network['realm']] ,$network['id']));
317 while ($row = $result->fetch (PDO::FETCH_ASSOC))
318 {
319 $object_id = $row['object_id'];
320
321 # Objects with zero attributes also matter due to the LEFT JOIN. Create
322 # keys for them too to enable negative caching.
323 if ($row['attr_id'] == NULL)
324 continue;
325
326 $record = array();
327 $record['id'] = $row['attr_id'];
328 $record['name'] = $row['attr_name'];
329 $record['type'] = $row['attr_type'];
330 switch ($row['attr_type'])
331 {
332 case 'dict':
333 $record['chapter_id'] = $row['chapter_id'];
334 $record['chapter_name'] = $row['chapter_name'];
335 $record['key'] = $row['uint_value'];
336 // fall through
337 case 'uint':
338 case 'float':
339 case 'string':
340 $record['value'] = $row[$row['attr_type'] . '_value'];
341 parseWikiLink ($record);
342 break;
343 case 'date':
344 $record['value'] = $row['uint_value'];
345 break;
346 default:
347 $record['value'] = NULL;
348 break;
349 }
350 $ret[$row['attr_id']] = $record;
351 }
352 return $ret;
353}
354
355// set/update/delete attribute value
356// source: commitUpdateAttrValue
357function commitUpdateAttrForNetwork ($network, $attr_id, $value = '')
358{
359 switch ($network['realm'])
360 {
361 case 'ipv4net':
362 $av_table = 'AttributeValue_IPv4';
363 break;
364 case 'ipv6net':
365 $av_table = 'AttributeValue_IPv6';
366 break;
367 default:
368 throw new InvalidArgException ('realm', $network['realm'], "Unknown realm");
369 }
370 $key = array ('net_id' => $network['id'], 'attr_id' => $attr_id);
371
372 $result = usePreparedSelectBlade
373 (
374 "SELECT type AS attr_type, av.* FROM Attribute a " .
375 "LEFT JOIN $av_table av ON a.id = av.attr_id AND av.net_id = ?" .
376 "WHERE a.id = ?",
377 array ($network['id'], $attr_id)
378 );
379 if (! $row = $result->fetch (PDO::FETCH_ASSOC))
380 throw new InvalidArgException ('$attr_id', $attr_id, 'No such attribute #'.$attr_id);
381 $attr_type = $row['attr_type'];
382 unset ($result);
383 switch ($attr_type)
384 {
385 case 'uint':
386 case 'float':
387 case 'string':
388 $column = $attr_type . '_value';
389 break;
390 case 'dict':
391 case 'date':
392 $column = 'uint_value';
393 break;
394 default:
395 throw new InvalidArgException ('$attr_type', $attr_type, 'Unknown attribute type found in ' . $network['realm'] . ' #' . $network['id'] . ', attribute #'.$attr_id);
396 }
397 $ret = 0;
398 if (isset ($row['attr_id']))
399 {
400 // AttributeValue row present in table
401 if ($value == '')
402 $ret = usePreparedDeleteBlade ($av_table, $key);
403 else
404 $ret = usePreparedUpdateBlade ($av_table, array ($column => $value), $key);
405 }
406 elseif ($value != '')
407 $ret = usePreparedInsertBlade ($av_table, $key + array ($column => $value));
408 return $ret;
409}
410
411// returns an array of network rows with attr_value filtered by attribute key or value
412// if $attribute_value is NULL, returns rows w/o specified attriute_id set
413// if $use_key is TRUE, $attribute_value is treated as dict_key, otherwise - as dict_value or actual value
414// if dont_filter is TRUE, all network rows are fetched. Useful to fetch all the values of a given attribute.
415function fetchNetworkRowsByAttr ($attribute_id, $attribute_value, $use_key = FALSE, $dont_filter = FALSE)
416{
417 global $netobject_type_id, $SQLSchema;
418
419 // get attribute type
420 static $map;
421 if (! isset ($map))
422 $map = getAttrMap();
423 if (! array_key_exists ($attribute_id, $map))
424 throw new InvalidArgException ('attribute_id', $attribute_id, "No such attribute");
425 $attribute = $map[$attribute_id];
426
427 // get realms
428 $realms = array();
429 foreach ($attribute['application'] as $application)
430 foreach ($netobject_type_id as $realm => $type)
431 if ($application['objtype_id'] == $type)
432 $realms[] = $realm;
433
434 $join_side = ($dont_filter && $attribute_value !== NULL) ? 'INNER' : 'LEFT';
435
436 $join = '';
437 $field = '';
438 switch ($attribute['type'])
439 {
440 case 'string':
441 $field = 'AV.string_value';
442 break;
443 case 'uint':
444 $field = 'AV.uint_value';
445 break;
446 case 'float':
447 $field = 'AV.float_value';
448 break;
449 case 'date':
450 $field = 'AV.uint_value';
451 break;
452 case 'dict':
453 if ($use_key)
454 $field = 'AV.uint_value';
455 else
456 {
457 $join = 'LEFT JOIN Dictionary D ON D.dict_key = AV.uint_value';
458 $field = 'D.dict_value';
459 }
460 break;
461 default:
462 throw new RackTablesError ();
463 }
464 $subqueries = array();
465 $params = array();
466 foreach (array ('ipv4net' => 'AttributeValue_IPv4', 'ipv6net' => 'AttributeValue_IPv6') as $realm => $table)
467 if (in_array ($realm, $realms))
468 {
469 $main_table = $SQLSchema[$realm]['table'];
470 $subquery = "
471SELECT
472 MT.id as net_id,
473 MT.ip,
474 MT.mask,
475 ? as realm,
476 $field as attr_value
477FROM
478 `$main_table` MT
479 $join_side JOIN `$table` AV ON MT.id = AV.net_id AND AV.attr_id = ?
480 $join
481";
482 $params[] = $realm;
483 $params[] = $attribute_id;
484
485 if (! $dont_filter)
486 {
487 if (isset ($attribute_value))
488 {
489 $subquery .= " WHERE $field = ?";
490 $params[] = $attribute_value;
491 }
492 else
493 $subquery .= " WHERE $field IS NULL";
494 }
495 $subqueries[] = $subquery;
496 }
497 $query = implode (' UNION ', $subqueries);
498 $result = usePreparedSelectBlade ($query, $params);
499 return $result->fetchAll (PDO::FETCH_ASSOC);
500}
501
502function fetchNetworksByAttr ($attribute_id, $attribute_value, $use_key = FALSE)
503{
504 $ret = array();
505 foreach (fetchNetworkRowsByAttr ($attribute_id, $attribute_value, $use_key) as $row)
506 {
507 $net_cell = spotEntity ($row['realm'], $row['net_id']);
6b991318 508 $ret[$net_cell['realm'] . '-' . $net_cell['id']] = $net_cell;
a2b02668
AA
509 }
510 return $ret;
511}