r3977 Final preparation before 0.18.5 release
[racktables] / inc / functions.php
1 <?php
2 /*
3 *
4 * This file is a library of computational functions for RackTables.
5 *
6 */
7
8 $loclist[0] = 'front';
9 $loclist[1] = 'interior';
10 $loclist[2] = 'rear';
11 $loclist['front'] = 0;
12 $loclist['interior'] = 1;
13 $loclist['rear'] = 2;
14 $template[0] = array (TRUE, TRUE, TRUE);
15 $template[1] = array (TRUE, TRUE, FALSE);
16 $template[2] = array (FALSE, TRUE, TRUE);
17 $template[3] = array (TRUE, FALSE, FALSE);
18 $template[4] = array (FALSE, TRUE, FALSE);
19 $template[5] = array (FALSE, FALSE, TRUE);
20 $templateWidth[0] = 3;
21 $templateWidth[1] = 2;
22 $templateWidth[2] = 2;
23 $templateWidth[3] = 1;
24 $templateWidth[4] = 1;
25 $templateWidth[5] = 1;
26
27 // Entity type by page number mapping is 1:1 atm, but may change later.
28 $etype_by_pageno = array
29 (
30 'ipv4net' => 'ipv4net',
31 'ipv4rspool' => 'ipv4rspool',
32 'ipv4vs' => 'ipv4vs',
33 'object' => 'object',
34 'rack' => 'rack',
35 'user' => 'user',
36 'file' => 'file',
37 );
38
39 // Rack thumbnail image width summands: "front", "interior" and "rear" elements w/o surrounding border.
40 $rtwidth = array
41 (
42 0 => 9,
43 1 => 21,
44 2 => 9
45 );
46
47 $netmaskbylen = array
48 (
49 32 => '255.255.255.255',
50 31 => '255.255.255.254',
51 30 => '255.255.255.252',
52 29 => '255.255.255.248',
53 28 => '255.255.255.240',
54 27 => '255.255.255.224',
55 26 => '255.255.255.192',
56 25 => '255.255.255.128',
57 24 => '255.255.255.0',
58 23 => '255.255.254.0',
59 22 => '255.255.252.0',
60 21 => '255.255.248.0',
61 20 => '255.255.240.0',
62 19 => '255.255.224.0',
63 18 => '255.255.192.0',
64 17 => '255.255.128.0',
65 16 => '255.255.0.0',
66 15 => '255.254.0.0',
67 14 => '255.252.0.0',
68 13 => '255.248.0.0',
69 12 => '255.240.0.0',
70 11 => '255.224.0.0',
71 10 => '255.192.0.0',
72 9 => '255.128.0.0',
73 8 => '255.0.0.0',
74 7 => '254.0.0.0',
75 6 => '252.0.0.0',
76 5 => '248.0.0.0',
77 4 => '240.0.0.0',
78 3 => '224.0.0.0',
79 2 => '192.0.0.0',
80 1 => '128.0.0.0'
81 );
82
83 $wildcardbylen = array
84 (
85 32 => '0.0.0.0',
86 31 => '0.0.0.1',
87 30 => '0.0.0.3',
88 29 => '0.0.0.7',
89 28 => '0.0.0.15',
90 27 => '0.0.0.31',
91 26 => '0.0.0.63',
92 25 => '0.0.0.127',
93 24 => '0.0.0.255',
94 23 => '0.0.1.255',
95 22 => '0.0.3.255',
96 21 => '0.0.7.255',
97 20 => '0.0.15.255',
98 19 => '0.0.31.255',
99 18 => '0.0.63.255',
100 17 => '0.0.127.255',
101 16 => '0.0.255.25',
102 15 => '0.1.255.255',
103 14 => '0.3.255.255',
104 13 => '0.7.255.255',
105 12 => '0.15.255.255',
106 11 => '0.31.255.255',
107 10 => '0.63.255.255',
108 9 => '0.127.255.255',
109 8 => '0.255.255.255',
110 7 => '1.255.255.255',
111 6 => '3.255.255.255',
112 5 => '7.255.255.255',
113 4 => '15.255.255.255',
114 3 => '31.255.255.255',
115 2 => '63.255.255.255',
116 1 => '127.255.255.255'
117 );
118
119 // This function assures that specified argument was passed
120 // and is a number greater than zero.
121 function assertUIntArg ($argname, $allow_zero = FALSE)
122 {
123 if (!isset ($_REQUEST[$argname]))
124 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is missing');
125 if (!is_numeric ($_REQUEST[$argname]))
126 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is not a number');
127 if ($_REQUEST[$argname] < 0)
128 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is less than zero');
129 if (!$allow_zero and $_REQUEST[$argname] == 0)
130 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is zero');
131 }
132
133 // This function assures that specified argument was passed
134 // and is a non-empty string.
135 function assertStringArg ($argname, $ok_if_empty = FALSE)
136 {
137 if (!isset ($_REQUEST[$argname]))
138 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is missing');
139 if (!is_string ($_REQUEST[$argname]))
140 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is not a string');
141 if (!$ok_if_empty and !strlen ($_REQUEST[$argname]))
142 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is an empty string');
143 }
144
145 function assertBoolArg ($argname, $ok_if_empty = FALSE)
146 {
147 if (!isset ($_REQUEST[$argname]))
148 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is missing');
149 if (!is_string ($_REQUEST[$argname]) or $_REQUEST[$argname] != 'on')
150 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is not a string');
151 if (!$ok_if_empty and !strlen ($_REQUEST[$argname]))
152 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is an empty string');
153 }
154
155 function assertIPv4Arg ($argname, $ok_if_empty = FALSE)
156 {
157 assertStringArg ($argname, $ok_if_empty);
158 if (strlen ($_REQUEST[$argname]) and long2ip (ip2long ($_REQUEST[$argname])) !== $_REQUEST[$argname])
159 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'parameter is not a valid ipv4 address');
160 }
161
162 function assertPCREArg ($argname)
163 {
164 assertStringArg ($argname, TRUE); // empty pattern is Ok
165 if (FALSE === preg_match ($_REQUEST[$argname], 'test'))
166 throw new InvalidRequestArgException($argname, $_REQUEST[$argname], 'PCRE validation failed');
167 }
168
169 // Objects of some types should be explicitly shown as
170 // anonymous (labelless). This function is a single place where the
171 // decision about displayed name is made.
172 function setDisplayedName (&$cell)
173 {
174 if ($cell['name'] != '')
175 $cell['dname'] = $cell['name'];
176 else
177 {
178 $cell['atags'][] = array ('tag' => '$nameless');
179 if (considerConfiguredConstraint ($cell, 'NAMEWARN_LISTSRC'))
180 $cell['dname'] = 'ANONYMOUS ' . decodeObjectType ($cell['objtype_id'], 'o');
181 else
182 $cell['dname'] = '[' . decodeObjectType ($cell['objtype_id'], 'o') . ']';
183 }
184 }
185
186 // This function finds height of solid rectangle of atoms, which are all
187 // assigned to the same object. Rectangle base is defined by specified
188 // template.
189 function rectHeight ($rackData, $startRow, $template_idx)
190 {
191 $height = 0;
192 // The first met object_id is used to match all the folowing IDs.
193 $object_id = 0;
194 global $template;
195 do
196 {
197 for ($locidx = 0; $locidx < 3; $locidx++)
198 {
199 // At least one value in template is TRUE, but the following block
200 // can meet 'skipped' atoms. Let's ensure we have something after processing
201 // the first row.
202 if ($template[$template_idx][$locidx])
203 {
204 if (isset ($rackData[$startRow - $height][$locidx]['skipped']))
205 break 2;
206 if (isset ($rackData[$startRow - $height][$locidx]['rowspan']))
207 break 2;
208 if (isset ($rackData[$startRow - $height][$locidx]['colspan']))
209 break 2;
210 if ($rackData[$startRow - $height][$locidx]['state'] != 'T')
211 break 2;
212 if ($object_id == 0)
213 $object_id = $rackData[$startRow - $height][$locidx]['object_id'];
214 if ($object_id != $rackData[$startRow - $height][$locidx]['object_id'])
215 break 2;
216 }
217 }
218 // If the first row can't offer anything, bail out.
219 if ($height == 0 and $object_id == 0)
220 break;
221 $height++;
222 }
223 while ($startRow - $height > 0);
224 # echo "for startRow==${startRow} and template==(" . ($template[$template_idx][0] ? 'T' : 'F');
225 # echo ', ' . ($template[$template_idx][1] ? 'T' : 'F') . ', ' . ($template[$template_idx][2] ? 'T' : 'F');
226 # echo ") height==${height}<br>\n";
227 return $height;
228 }
229
230 // This function marks atoms to be avoided by rectHeight() and assigns rowspan/colspan
231 // attributes.
232 function markSpan (&$rackData, $startRow, $maxheight, $template_idx)
233 {
234 global $template, $templateWidth;
235 $colspan = 0;
236 for ($height = 0; $height < $maxheight; $height++)
237 {
238 for ($locidx = 0; $locidx < 3; $locidx++)
239 {
240 if ($template[$template_idx][$locidx])
241 {
242 // Add colspan/rowspan to the first row met and mark the following ones to skip.
243 // Explicitly show even single-cell spanned atoms, because rectHeight()
244 // is expeciting this data for correct calculation.
245 if ($colspan != 0)
246 $rackData[$startRow - $height][$locidx]['skipped'] = TRUE;
247 else
248 {
249 $colspan = $templateWidth[$template_idx];
250 if ($colspan >= 1)
251 $rackData[$startRow - $height][$locidx]['colspan'] = $colspan;
252 if ($maxheight >= 1)
253 $rackData[$startRow - $height][$locidx]['rowspan'] = $maxheight;
254 }
255 }
256 }
257 }
258 return;
259 }
260
261 // This function sets rowspan/solspan/skipped atom attributes for renderRack()
262 // What we actually have to do is to find _all_ possible rectangles for each unit
263 // and then select the widest of those with the maximal square.
264 function markAllSpans (&$rackData = NULL)
265 {
266 if ($rackData == NULL)
267 {
268 showWarning ('Invalid rackData', __FUNCTION__);
269 return;
270 }
271 for ($i = $rackData['height']; $i > 0; $i--)
272 while (markBestSpan ($rackData, $i));
273 }
274
275 // Calculate height of 6 possible span templates (array is presorted by width
276 // descending) and mark the best (if any).
277 function markBestSpan (&$rackData, $i)
278 {
279 global $template, $templateWidth;
280 for ($j = 0; $j < 6; $j++)
281 {
282 $height[$j] = rectHeight ($rackData, $i, $j);
283 $square[$j] = $height[$j] * $templateWidth[$j];
284 }
285 // find the widest rectangle of those with maximal height
286 $maxsquare = max ($square);
287 if (!$maxsquare)
288 return FALSE;
289 $best_template_index = 0;
290 for ($j = 0; $j < 6; $j++)
291 if ($square[$j] == $maxsquare)
292 {
293 $best_template_index = $j;
294 $bestheight = $height[$j];
295 break;
296 }
297 // distribute span marks
298 markSpan ($rackData, $i, $bestheight, $best_template_index);
299 return TRUE;
300 }
301
302 // We can mount 'F' atoms and unmount our own 'T' atoms.
303 function applyObjectMountMask (&$rackData, $object_id)
304 {
305 for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
306 for ($locidx = 0; $locidx < 3; $locidx++)
307 switch ($rackData[$unit_no][$locidx]['state'])
308 {
309 case 'F':
310 $rackData[$unit_no][$locidx]['enabled'] = TRUE;
311 break;
312 case 'T':
313 $rackData[$unit_no][$locidx]['enabled'] = ($rackData[$unit_no][$locidx]['object_id'] == $object_id);
314 break;
315 default:
316 $rackData[$unit_no][$locidx]['enabled'] = FALSE;
317 }
318 }
319
320 // Design change means transition between 'F' and 'A' and back.
321 function applyRackDesignMask (&$rackData)
322 {
323 for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
324 for ($locidx = 0; $locidx < 3; $locidx++)
325 switch ($rackData[$unit_no][$locidx]['state'])
326 {
327 case 'F':
328 case 'A':
329 $rackData[$unit_no][$locidx]['enabled'] = TRUE;
330 break;
331 default:
332 $rackData[$unit_no][$locidx]['enabled'] = FALSE;
333 }
334 }
335
336 // The same for 'F' and 'U'.
337 function applyRackProblemMask (&$rackData)
338 {
339 for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
340 for ($locidx = 0; $locidx < 3; $locidx++)
341 switch ($rackData[$unit_no][$locidx]['state'])
342 {
343 case 'F':
344 case 'U':
345 $rackData[$unit_no][$locidx]['enabled'] = TRUE;
346 break;
347 default:
348 $rackData[$unit_no][$locidx]['enabled'] = FALSE;
349 }
350 }
351
352 // This function highlights specified object (and removes previous highlight).
353 function highlightObject (&$rackData, $object_id)
354 {
355 for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
356 for ($locidx = 0; $locidx < 3; $locidx++)
357 if
358 (
359 $rackData[$unit_no][$locidx]['state'] == 'T' and
360 $rackData[$unit_no][$locidx]['object_id'] == $object_id
361 )
362 $rackData[$unit_no][$locidx]['hl'] = 'h';
363 else
364 unset ($rackData[$unit_no][$locidx]['hl']);
365 }
366
367 // This function marks atoms to selected or not depending on their current state.
368 function markupAtomGrid (&$data, $checked_state)
369 {
370 for ($unit_no = $data['height']; $unit_no > 0; $unit_no--)
371 for ($locidx = 0; $locidx < 3; $locidx++)
372 {
373 if (!($data[$unit_no][$locidx]['enabled'] === TRUE))
374 continue;
375 if ($data[$unit_no][$locidx]['state'] == $checked_state)
376 $data[$unit_no][$locidx]['checked'] = ' checked';
377 else
378 $data[$unit_no][$locidx]['checked'] = '';
379 }
380 }
381
382 // This function is almost a clone of processGridForm(), but doesn't save anything to database
383 // Return value is the changed rack data.
384 // Here we assume that correct filter has already been applied, so we just
385 // set or unset checkbox inputs w/o changing atom state.
386 function mergeGridFormToRack (&$rackData)
387 {
388 $rack_id = $rackData['id'];
389 for ($unit_no = $rackData['height']; $unit_no > 0; $unit_no--)
390 for ($locidx = 0; $locidx < 3; $locidx++)
391 {
392 if ($rackData[$unit_no][$locidx]['enabled'] != TRUE)
393 continue;
394 $inputname = "atom_${rack_id}_${unit_no}_${locidx}";
395 if (isset ($_REQUEST[$inputname]) and $_REQUEST[$inputname] == 'on')
396 $rackData[$unit_no][$locidx]['checked'] = ' checked';
397 else
398 $rackData[$unit_no][$locidx]['checked'] = '';
399 }
400 }
401
402 // netmask conversion from length to number
403 function binMaskFromDec ($maskL)
404 {
405 $map_straight = array (
406 0 => 0x00000000,
407 1 => 0x80000000,
408 2 => 0xc0000000,
409 3 => 0xe0000000,
410 4 => 0xf0000000,
411 5 => 0xf8000000,
412 6 => 0xfc000000,
413 7 => 0xfe000000,
414 8 => 0xff000000,
415 9 => 0xff800000,
416 10 => 0xffc00000,
417 11 => 0xffe00000,
418 12 => 0xfff00000,
419 13 => 0xfff80000,
420 14 => 0xfffc0000,
421 15 => 0xfffe0000,
422 16 => 0xffff0000,
423 17 => 0xffff8000,
424 18 => 0xffffc000,
425 19 => 0xffffe000,
426 20 => 0xfffff000,
427 21 => 0xfffff800,
428 22 => 0xfffffc00,
429 23 => 0xfffffe00,
430 24 => 0xffffff00,
431 25 => 0xffffff80,
432 26 => 0xffffffc0,
433 27 => 0xffffffe0,
434 28 => 0xfffffff0,
435 29 => 0xfffffff8,
436 30 => 0xfffffffc,
437 31 => 0xfffffffe,
438 32 => 0xffffffff,
439 );
440 return $map_straight[$maskL];
441 }
442
443 // complementary value
444 function binInvMaskFromDec ($maskL)
445 {
446 $map_compl = array (
447 0 => 0xffffffff,
448 1 => 0x7fffffff,
449 2 => 0x3fffffff,
450 3 => 0x1fffffff,
451 4 => 0x0fffffff,
452 5 => 0x07ffffff,
453 6 => 0x03ffffff,
454 7 => 0x01ffffff,
455 8 => 0x00ffffff,
456 9 => 0x007fffff,
457 10 => 0x003fffff,
458 11 => 0x001fffff,
459 12 => 0x000fffff,
460 13 => 0x0007ffff,
461 14 => 0x0003ffff,
462 15 => 0x0001ffff,
463 16 => 0x0000ffff,
464 17 => 0x00007fff,
465 18 => 0x00003fff,
466 19 => 0x00001fff,
467 20 => 0x00000fff,
468 21 => 0x000007ff,
469 22 => 0x000003ff,
470 23 => 0x000001ff,
471 24 => 0x000000ff,
472 25 => 0x0000007f,
473 26 => 0x0000003f,
474 27 => 0x0000001f,
475 28 => 0x0000000f,
476 29 => 0x00000007,
477 30 => 0x00000003,
478 31 => 0x00000001,
479 32 => 0x00000000,
480 );
481 return $map_compl[$maskL];
482 }
483
484 // This function looks up 'has_problems' flag for 'T' atoms
485 // and modifies 'hl' key. May be, this should be better done
486 // in amplifyCell(). We don't honour 'skipped' key, because
487 // the function is also used for thumb creation.
488 function markupObjectProblems (&$rackData)
489 {
490 for ($i = $rackData['height']; $i > 0; $i--)
491 for ($locidx = 0; $locidx < 3; $locidx++)
492 if ($rackData[$i][$locidx]['state'] == 'T')
493 {
494 $object = spotEntity ('object', $rackData[$i][$locidx]['object_id']);
495 if ($object['has_problems'] == 'yes')
496 {
497 // Object can be already highlighted.
498 if (isset ($rackData[$i][$locidx]['hl']))
499 $rackData[$i][$locidx]['hl'] = $rackData[$i][$locidx]['hl'] . 'w';
500 else
501 $rackData[$i][$locidx]['hl'] = 'w';
502 }
503 }
504 }
505
506 // Return a uniformly (010203040506 or 0102030405060708) formatted address, if it is present
507 // in the provided string, an empty string for an empty string or NULL for error.
508 function l2addressForDatabase ($string)
509 {
510 $string = strtoupper ($string);
511 switch (TRUE)
512 {
513 case ($string == '' or preg_match (RE_L2_SOLID, $string) or preg_match (RE_L2_WWN_SOLID, $string)):
514 return $string;
515 case (preg_match (RE_L2_IFCFG, $string) or preg_match (RE_L2_WWN_COLON, $string)):
516 // reformat output of SunOS ifconfig
517 $ret = '';
518 foreach (explode (':', $string) as $byte)
519 $ret .= (strlen ($byte) == 1 ? '0' : '') . $byte;
520 return $ret;
521 case (preg_match (RE_L2_CISCO, $string)):
522 return str_replace ('.', '', $string);
523 case (preg_match (RE_L2_HUAWEI, $string)):
524 return str_replace ('-', '', $string);
525 case (preg_match (RE_L2_IPCFG, $string) or preg_match (RE_L2_WWN_HYPHEN, $string)):
526 return str_replace ('-', '', $string);
527 default:
528 return NULL;
529 }
530 }
531
532 function l2addressFromDatabase ($string)
533 {
534 switch (strlen ($string))
535 {
536 case 12: // Ethernet
537 case 16: // FireWire/Fibre Channel
538 $ret = implode (':', str_split ($string, 2));
539 break;
540 default:
541 $ret = $string;
542 break;
543 }
544 return $ret;
545 }
546
547 // The following 2 functions return previous and next rack IDs for
548 // a given rack ID. The order of racks is the same as in renderRackspace()
549 // or renderRow().
550 function getPrevIDforRack ($row_id = 0, $rack_id = 0)
551 {
552 if ($row_id <= 0 or $rack_id <= 0)
553 {
554 showWarning ('Invalid arguments passed', __FUNCTION__);
555 return NULL;
556 }
557 $rackList = listCells ('rack', $row_id);
558 doubleLink ($rackList);
559 if (isset ($rackList[$rack_id]['prev_key']))
560 return $rackList[$rack_id]['prev_key'];
561 return NULL;
562 }
563
564 function getNextIDforRack ($row_id = 0, $rack_id = 0)
565 {
566 if ($row_id <= 0 or $rack_id <= 0)
567 {
568 showWarning ('Invalid arguments passed', __FUNCTION__);
569 return NULL;
570 }
571 $rackList = listCells ('rack', $row_id);
572 doubleLink ($rackList);
573 if (isset ($rackList[$rack_id]['next_key']))
574 return $rackList[$rack_id]['next_key'];
575 return NULL;
576 }
577
578 // This function finds previous and next array keys for each array key and
579 // modifies its argument accordingly.
580 function doubleLink (&$array)
581 {
582 $prev_key = NULL;
583 foreach (array_keys ($array) as $key)
584 {
585 if ($prev_key)
586 {
587 $array[$key]['prev_key'] = $prev_key;
588 $array[$prev_key]['next_key'] = $key;
589 }
590 $prev_key = $key;
591 }
592 }
593
594 function sortTokenize ($a, $b)
595 {
596 $aold='';
597 while ($a != $aold)
598 {
599 $aold=$a;
600 $a = preg_replace('/[^a-zA-Z0-9]/',' ',$a);
601 $a = preg_replace('/([0-9])([a-zA-Z])/','\\1 \\2',$a);
602 $a = preg_replace('/([a-zA-Z])([0-9])/','\\1 \\2',$a);
603 }
604
605 $bold='';
606 while ($b != $bold)
607 {
608 $bold=$b;
609 $b = preg_replace('/[^a-zA-Z0-9]/',' ',$b);
610 $b = preg_replace('/([0-9])([a-zA-Z])/','\\1 \\2',$b);
611 $b = preg_replace('/([a-zA-Z])([0-9])/','\\1 \\2',$b);
612 }
613
614
615
616 $ar = explode(' ', $a);
617 $br = explode(' ', $b);
618 for ($i=0; $i<count($ar) && $i<count($br); $i++)
619 {
620 $ret = 0;
621 if (is_numeric($ar[$i]) and is_numeric($br[$i]))
622 $ret = ($ar[$i]==$br[$i])?0:($ar[$i]<$br[$i]?-1:1);
623 else
624 $ret = strcasecmp($ar[$i], $br[$i]);
625 if ($ret != 0)
626 return $ret;
627 }
628 if ($i<count($ar))
629 return 1;
630 if ($i<count($br))
631 return -1;
632 return 0;
633 }
634
635 // This function returns an array of single element of object's FQDN attribute,
636 // if FQDN is set. The next choice is object's common name, if it looks like a
637 // hostname. Otherwise an array of all 'regular' IP addresses of the
638 // object is returned (which may appear 0 and more elements long).
639 function findAllEndpoints ($object_id, $fallback = '')
640 {
641 foreach (getAttrValues ($object_id) as $record)
642 if ($record['id'] == 3 && strlen ($record['value'])) // FQDN
643 return array ($record['value']);
644 $regular = array();
645 foreach (getObjectIPv4Allocations ($object_id) as $dottedquad => $alloc)
646 if ($alloc['type'] == 'regular')
647 $regular[] = $dottedquad;
648 if (!count ($regular) && strlen ($fallback))
649 return array ($fallback);
650 return $regular;
651 }
652
653 // Some records in the dictionary may be written as plain text or as Wiki
654 // link in the following syntax:
655 // 1. word
656 // 2. [[word URL]] // FIXME: this isn't working
657 // 3. [[word word word | URL]]
658 // This function parses the line and returns text suitable for either A
659 // (rendering <A HREF>) or O (for <OPTION>).
660 function parseWikiLink ($line, $which)
661 {
662 if (preg_match ('/^\[\[.+\]\]$/', $line) == 0)
663 {
664 // always strip the marker for A-data, but let cookOptgroup()
665 // do this later (otherwise it can't sort groups out)
666 if ($which == 'a')
667 return preg_replace ('/^.+%GSKIP%/', '', preg_replace ('/^(.+)%GPASS%/', '\\1 ', $line));
668 else
669 return $line;
670 }
671 $line = preg_replace ('/^\[\[(.+)\]\]$/', '$1', $line);
672 $s = explode ('|', $line);
673 $o_value = trim ($s[0]);
674 if ($which == 'o')
675 return $o_value;
676 $o_value = preg_replace ('/^.+%GSKIP%/', '', preg_replace ('/^(.+)%GPASS%/', '\\1 ', $o_value));
677 $a_value = trim ($s[1]);
678 return "<a href='${a_value}'>${o_value}</a>";
679 }
680
681 // FIXME: should this be saved as "P-data"?
682 function execGMarker ($line)
683 {
684 return preg_replace ('/^.+%GSKIP%/', '', preg_replace ('/^(.+)%GPASS%/', '\\1 ', $line));
685 }
686
687 // rackspace usage for a single rack
688 // (T + W + U) / (height * 3 - A)
689 function getRSUforRack ($data = NULL)
690 {
691 if ($data == NULL)
692 {
693 showWarning ('Invalid argument', __FUNCTION__);
694 return NULL;
695 }
696 $counter = array ('A' => 0, 'U' => 0, 'T' => 0, 'W' => 0, 'F' => 0);
697 for ($unit_no = $data['height']; $unit_no > 0; $unit_no--)
698 for ($locidx = 0; $locidx < 3; $locidx++)
699 $counter[$data[$unit_no][$locidx]['state']]++;
700 return ($counter['T'] + $counter['W'] + $counter['U']) / ($counter['T'] + $counter['W'] + $counter['U'] + $counter['F']);
701 }
702
703 // Same for row.
704 function getRSUforRackRow ($rowData = NULL)
705 {
706 if ($rowData === NULL)
707 {
708 showWarning ('Invalid argument', __FUNCTION__);
709 return NULL;
710 }
711 if (!count ($rowData))
712 return 0;
713 $counter = array ('A' => 0, 'U' => 0, 'T' => 0, 'W' => 0, 'F' => 0);
714 $total_height = 0;
715 foreach (array_keys ($rowData) as $rack_id)
716 {
717 $data = spotEntity ('rack', $rack_id);
718 amplifyCell ($data);
719 $total_height += $data['height'];
720 for ($unit_no = $data['height']; $unit_no > 0; $unit_no--)
721 for ($locidx = 0; $locidx < 3; $locidx++)
722 $counter[$data[$unit_no][$locidx]['state']]++;
723 }
724 return ($counter['T'] + $counter['W'] + $counter['U']) / ($counter['T'] + $counter['W'] + $counter['U'] + $counter['F']);
725 }
726
727 // Make sure the string is always wrapped with LF characters
728 function lf_wrap ($str)
729 {
730 $ret = trim ($str, "\r\n");
731 if (strlen ($ret))
732 $ret .= "\n";
733 return $ret;
734 }
735
736 // Adopted from Mantis BTS code.
737 function string_insert_hrefs ($s)
738 {
739 if (getConfigVar ('DETECT_URLS') != 'yes')
740 return $s;
741 # Find any URL in a string and replace it by a clickable link
742 $s = preg_replace( '/(([[:alpha:]][-+.[:alnum:]]*):\/\/(%[[:digit:]A-Fa-f]{2}|[-_.!~*\';\/?%^\\\\:@&={\|}+$#\(\),\[\][:alnum:]])+)/se',
743 "'<a href=\"'.rtrim('\\1','.').'\">\\1</a> [<a href=\"'.rtrim('\\1','.').'\" target=\"_blank\">^</a>]'",
744 $s);
745 $s = preg_replace( '/\b' . email_regex_simple() . '\b/i',
746 '<a href="mailto:\0">\0</a>',
747 $s);
748 return $s;
749 }
750
751 // Idem.
752 function email_regex_simple ()
753 {
754 return "(([a-z0-9!#*+\/=?^_{|}~-]+(?:\.[a-z0-9!#*+\/=?^_{|}~-]+)*)" . # recipient
755 "\@((?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?))"; # @domain
756 }
757
758 // Parse AUTOPORTS_CONFIG and return a list of generated pairs (port_type, port_name)
759 // for the requested object_type_id.
760 function getAutoPorts ($type_id)
761 {
762 $ret = array();
763 $typemap = explode (';', str_replace (' ', '', getConfigVar ('AUTOPORTS_CONFIG')));
764 foreach ($typemap as $equation)
765 {
766 $tmp = explode ('=', $equation);
767 if (count ($tmp) != 2)
768 continue;
769 $objtype_id = $tmp[0];
770 if ($objtype_id != $type_id)
771 continue;
772 $portlist = $tmp[1];
773 foreach (explode ('+', $portlist) as $product)
774 {
775 $tmp = explode ('*', $product);
776 if (count ($tmp) != 3)
777 continue;
778 $nports = $tmp[0];
779 $port_type = $tmp[1];
780 $format = $tmp[2];
781 for ($i = 0; $i < $nports; $i++)
782 $ret[] = array ('type' => $port_type, 'name' => @sprintf ($format, $i));
783 }
784 }
785 return $ret;
786 }
787
788 // Use pre-served trace to traverse the tree, then place given node where it belongs.
789 function pokeNode (&$tree, $trace, $key, $value, $threshold = 0)
790 {
791 // This function needs the trace to be followed FIFO-way. The fastest
792 // way to do so is to use array_push() for putting values into the
793 // list and array_shift() for getting them out. This exposed up to 11%
794 // performance gain compared to other patterns of array_push/array_unshift/
795 // array_reverse/array_pop/array_shift conjunction.
796 $myid = array_shift ($trace);
797 if (!count ($trace)) // reached the target
798 {
799 if (!$threshold or ($threshold and $tree[$myid]['kidc'] + 1 < $threshold))
800 $tree[$myid]['kids'][$key] = $value;
801 // Reset accumulated records once, when the limit is reached, not each time
802 // after that.
803 if (++$tree[$myid]['kidc'] == $threshold)
804 $tree[$myid]['kids'] = array();
805 }
806 else // not yet
807 {
808 $self = __FUNCTION__;
809 $self ($tree[$myid]['kids'], $trace, $key, $value, $threshold);
810 }
811 }
812
813 // Likewise traverse the tree with the trace and return the final node.
814 function peekNode ($tree, $trace, $target_id)
815 {
816 $self = __FUNCTION__;
817 if (NULL === ($next = array_shift ($trace))) // warm
818 {
819 foreach ($tree as $node)
820 if (array_key_exists ('id', $node) and $node['id'] == $target_id) // hot
821 return $node;
822 }
823 else // cold
824 {
825 foreach ($tree as $node)
826 if (array_key_exists ('id', $node) and $node['id'] == $next) // warmer
827 return $self ($node['kids'], $trace, $target_id);
828 }
829 // HCF
830 return NULL;
831 }
832
833 // Build a tree from the item list and return it. Input and output data is
834 // indexed by item id (nested items in output are recursively stored in 'kids'
835 // key, which is in turn indexed by id. Functions, which are ready to handle
836 // tree collapsion/expansion themselves, may request non-zero threshold value
837 // for smaller resulting tree.
838 function treeFromList (&$orig_nodelist, $threshold = 0, $return_main_payload = TRUE)
839 {
840 $tree = array();
841 $nodelist = $orig_nodelist;
842 // Array equivalent of traceEntity() function.
843 $trace = array();
844 // set kidc and kids only once
845 foreach (array_keys ($nodelist) as $nodeid)
846 {
847 $nodelist[$nodeid]['kidc'] = 0;
848 $nodelist[$nodeid]['kids'] = array();
849 }
850 do
851 {
852 $nextpass = FALSE;
853 foreach (array_keys ($nodelist) as $nodeid)
854 {
855 // When adding a node to the working tree, book another
856 // iteration, because the new item could make a way for
857 // others onto the tree. Also remove any item added from
858 // the input list, so iteration base shrinks.
859 // First check if we can assign directly.
860 if ($nodelist[$nodeid]['parent_id'] == NULL)
861 {
862 $tree[$nodeid] = $nodelist[$nodeid];
863 $trace[$nodeid] = array(); // Trace to root node is empty
864 unset ($nodelist[$nodeid]);
865 $nextpass = TRUE;
866 }
867 // Now look if it fits somewhere on already built tree.
868 elseif (isset ($trace[$nodelist[$nodeid]['parent_id']]))
869 {
870 // Trace to a node is a trace to its parent plus parent id.
871 $trace[$nodeid] = $trace[$nodelist[$nodeid]['parent_id']];
872 $trace[$nodeid][] = $nodelist[$nodeid]['parent_id'];
873 pokeNode ($tree, $trace[$nodeid], $nodeid, $nodelist[$nodeid], $threshold);
874 // path to any other node is made of all parent nodes plus the added node itself
875 unset ($nodelist[$nodeid]);
876 $nextpass = TRUE;
877 }
878 }
879 }
880 while ($nextpass);
881 if (!$return_main_payload)
882 return $nodelist;
883 // update each input node with its backtrace route
884 foreach ($trace as $nodeid => $route)
885 $orig_nodelist[$nodeid]['trace'] = $route;
886 return $tree;
887 }
888
889 // Build a tree from the tag list and return everything _except_ the tree.
890 // IOW, return taginfo items, which have parent_id set and pointing outside
891 // of the "normal" tree, which originates from the root.
892 function getOrphanedTags ()
893 {
894 global $taglist;
895 return treeFromList ($taglist, 0, FALSE);
896 }
897
898 function serializeTags ($chain, $baseurl = '')
899 {
900 $comma = '';
901 $ret = '';
902 foreach ($chain as $taginfo)
903 {
904 $ret .= $comma .
905 ($baseurl == '' ? '' : "<a href='${baseurl}cft[]=${taginfo['id']}'>") .
906 $taginfo['tag'] .
907 ($baseurl == '' ? '' : '</a>');
908 $comma = ', ';
909 }
910 return $ret;
911 }
912
913 // Return the list of missing implicit tags.
914 function getImplicitTags ($oldtags)
915 {
916 global $taglist;
917 $tmp = array();
918 foreach ($oldtags as $taginfo)
919 $tmp = array_merge ($tmp, $taglist[$taginfo['id']]['trace']);
920 // don't call array_unique here, it is in the function we will call now
921 return buildTagChainFromIds ($tmp);
922 }
923
924 // Minimize the chain: exclude all implicit tags and return the result.
925 // This function makes use of an external cache with a miss/hit ratio
926 // about 3/7 (ticket:255).
927 function getExplicitTagsOnly ($chain)
928 {
929 global $taglist, $tagRelCache;
930 $ret = array();
931 foreach (array_keys ($chain) as $keyA) // check each A
932 {
933 $tagidA = $chain[$keyA]['id'];
934 // do not include A in result, if A is seen on the trace of any B!=A
935 foreach (array_keys ($chain) as $keyB)
936 {
937 $tagidB = $chain[$keyB]['id'];
938 if ($tagidA == $tagidB)
939 continue;
940 if (!isset ($tagRelCache[$tagidA][$tagidB]))
941 $tagRelCache[$tagidA][$tagidB] = in_array ($tagidA, $taglist[$tagidB]['trace']);
942 if ($tagRelCache[$tagidA][$tagidB] === TRUE) // A is ancestor of B
943 continue 2; // skip this A
944 }
945 $ret[] = $chain[$keyA];
946 }
947 return $ret;
948 }
949
950 // Universal autotags generator, a complementing function for loadEntityTags().
951 // Bypass key isn't strictly typed, but interpreted depending on the realm.
952 function generateEntityAutoTags ($cell)
953 {
954 $ret = array();
955 switch ($cell['realm'])
956 {
957 case 'rack':
958 $ret[] = array ('tag' => '$rackid_' . $cell['id']);
959 $ret[] = array ('tag' => '$any_rack');
960 break;
961 case 'object':
962 $ret[] = array ('tag' => '$id_' . $cell['id']);
963 $ret[] = array ('tag' => '$typeid_' . $cell['objtype_id']);
964 $ret[] = array ('tag' => '$any_object');
965 if (validTagName ('$cn_' . $cell['name'], TRUE))
966 $ret[] = array ('tag' => '$cn_' . $cell['name']);
967 if (!strlen ($cell['rack_id']))
968 $ret[] = array ('tag' => '$unmounted');
969 if (!$cell['nports'])
970 $ret[] = array ('tag' => '$portless');
971 if ($cell['asset_no'] == '')
972 $ret[] = array ('tag' => '$no_asset_tag');
973 if ($cell['runs8021Q'])
974 $ret[] = array ('tag' => '$runs_8021Q');
975
976 // dictionary attribute autotags '$attr_X_Y'
977 $attrs = getAttrValues($cell['id']);
978 foreach ($attrs as $attr_id => $attr_record)
979 if (isset ($attr_record['key']))
980 $ret[] = array ('tag' => "\$attr_{$attr_id}_{$attr_record['key']}");
981 break;
982 case 'ipv4net':
983 $ret[] = array ('tag' => '$ip4netid_' . $cell['id']);
984 $ret[] = array ('tag' => '$ip4net-' . str_replace ('.', '-', $cell['ip']) . '-' . $cell['mask']);
985 for ($i = 8; $i < 32; $i++)
986 {
987 // these conditions hit 1 to 3 times per each i
988 if ($cell['mask'] >= $i)
989 $ret[] = array ('tag' => '$masklen_ge_' . $i);
990 if ($cell['mask'] <= $i)
991 $ret[] = array ('tag' => '$masklen_le_' . $i);
992 if ($cell['mask'] == $i)
993 $ret[] = array ('tag' => '$masklen_eq_' . $i);
994 }
995 $ret[] = array ('tag' => '$any_ip4net');
996 $ret[] = array ('tag' => '$any_net');
997 break;
998 case 'ipv4vs':
999 $ret[] = array ('tag' => '$ipv4vsid_' . $cell['id']);
1000 $ret[] = array ('tag' => '$any_ipv4vs');
1001 $ret[] = array ('tag' => '$any_vs');
1002 break;
1003 case 'ipv4rspool':
1004 $ret[] = array ('tag' => '$ipv4rspid_' . $cell['id']);
1005 $ret[] = array ('tag' => '$any_ipv4rsp');
1006 $ret[] = array ('tag' => '$any_rsp');
1007 break;
1008 case 'user':
1009 // {$username_XXX} autotag is generated always, but {$userid_XXX}
1010 // appears only for accounts, which exist in local database.
1011 $ret[] = array ('tag' => '$username_' . $cell['user_name']);
1012 if (isset ($cell['user_id']))
1013 $ret[] = array ('tag' => '$userid_' . $cell['user_id']);
1014 break;
1015 case 'file':
1016 $ret[] = array ('tag' => '$fileid_' . $cell['id']);
1017 $ret[] = array ('tag' => '$any_file');
1018 break;
1019 default: // HCF!
1020 break;
1021 }
1022 // {$tagless} doesn't apply to users
1023 switch ($cell['realm'])
1024 {
1025 case 'rack':
1026 case 'object':
1027 case 'ipv4net':
1028 case 'ipv4vs':
1029 case 'ipv4rspool':
1030 case 'file':
1031 if (!count ($cell['etags']))
1032 $ret[] = array ('tag' => '$untagged');
1033 break;
1034 default:
1035 break;
1036 }
1037 return $ret;
1038 }
1039
1040 // Check, if the given tag is present on the chain (will only work
1041 // for regular tags with tag ID set.
1042 function tagOnChain ($taginfo, $tagchain)
1043 {
1044 if (!isset ($taginfo['id']))
1045 return FALSE;
1046 foreach ($tagchain as $test)
1047 if ($test['id'] == $taginfo['id'])
1048 return TRUE;
1049 return FALSE;
1050 }
1051
1052 function tagNameOnChain ($tagname, $tagchain)
1053 {
1054 foreach ($tagchain as $test)
1055 if ($test['tag'] == $tagname)
1056 return TRUE;
1057 return FALSE;
1058 }
1059
1060 // Return TRUE, if two tags chains differ (order of tags doesn't matter).
1061 // Assume, that neither of the lists contains duplicates.
1062 // FIXME: a faster, than O(x^2) method is possible for this calculation.
1063 function tagChainCmp ($chain1, $chain2)
1064 {
1065 if (count ($chain1) != count ($chain2))
1066 return TRUE;
1067 foreach ($chain1 as $taginfo1)
1068 if (!tagOnChain ($taginfo1, $chain2))
1069 return TRUE;
1070 return FALSE;
1071 }
1072
1073 function redirectIfNecessary ()
1074 {
1075 global
1076 $trigger,
1077 $pageno,
1078 $tabno;
1079 $pmap = array
1080 (
1081 'accounts' => 'userlist',
1082 'rspools' => 'ipv4rsplist',
1083 'rspool' => 'ipv4rsp',
1084 'vservices' => 'ipv4vslist',
1085 'vservice' => 'ipv4vs',
1086 'objects' => 'depot',
1087 'objgroup' => 'depot',
1088 );
1089 $tmap = array();
1090 $tmap['objects']['newmulti'] = 'addmore';
1091 $tmap['objects']['newobj'] = 'addmore';
1092 $tmap['object']['switchvlans'] = 'livevlans';
1093 $tmap['object']['slb'] = 'editrspvs';
1094 $tmap['object']['portfwrd'] = 'nat4';
1095 $tmap['object']['network'] = 'ipv4';
1096 if (isset ($pmap[$pageno]))
1097 redirectUser ($pmap[$pageno], $tabno);
1098 if (isset ($tmap[$pageno][$tabno]))
1099 redirectUser ($pageno, $tmap[$pageno][$tabno]);
1100 // check if we accidentaly got on a dynamic tab that shouldn't be shown for this object
1101 if
1102 (
1103 isset ($trigger[$pageno][$tabno]) and
1104 !strlen (call_user_func ($trigger[$pageno][$tabno]))
1105 )
1106 redirectUser ($pageno, 'default');
1107 }
1108
1109 function prepareNavigation()
1110 {
1111 global
1112 $pageno,
1113 $tabno;
1114
1115 $pageno = (isset ($_REQUEST['page'])) ? $_REQUEST['page'] : 'index';
1116
1117 // Special handling of tab number to substitute the "last" index where applicable.
1118 // Always show explicitly requested tab, substitute the last used name in case
1119 // it is awailable, fall back to the default one.
1120
1121 if (isset ($_REQUEST['tab']))
1122 $tabno = $_REQUEST['tab'];
1123 elseif
1124 (
1125 basename($_SERVER['PHP_SELF']) == 'index.php' and
1126 getConfigVar ('SHOW_LAST_TAB') == 'yes' and
1127 isset ($_SESSION['RTLT'][$pageno]) and
1128 permitted ($pageno, $_SESSION['RTLT'][$pageno])
1129 )
1130 redirectUser ($pageno, $_SESSION['RTLT'][$pageno]);
1131 else
1132 $tabno = 'default';
1133 }
1134
1135 function fixContext ($target = NULL)
1136 {
1137 global
1138 $pageno,
1139 $auto_tags,
1140 $expl_tags,
1141 $impl_tags,
1142 $target_given_tags,
1143 $user_given_tags,
1144 $etype_by_pageno,
1145 $page;
1146
1147 if ($target !== NULL)
1148 {
1149 $target_given_tags = $target['etags'];
1150 // Don't reset autochain, because auth procedures could push stuff there in.
1151 // Another important point is to ignore 'user' realm, so we don't infuse effective
1152 // context with autotags of the displayed account.
1153 if ($target['realm'] != 'user')
1154 $auto_tags = array_merge ($auto_tags, $target['atags']);
1155 }
1156 elseif (array_key_exists ($pageno, $etype_by_pageno))
1157 {
1158 // Each page listed in the map above requires one uint argument.
1159 $target_realm = $etype_by_pageno[$pageno];
1160 assertUIntArg ($page[$pageno]['bypass']);
1161 $target_id = $_REQUEST[$page[$pageno]['bypass']];
1162 $target = spotEntity ($target_realm, $target_id);
1163 $target_given_tags = $target['etags'];
1164 if ($target['realm'] != 'user')
1165 $auto_tags = array_merge ($auto_tags, $target['atags']);
1166 }
1167 // Explicit and implicit chains should be normally empty at this point, so
1168 // overwrite the contents anyway.
1169 $expl_tags = mergeTagChains ($user_given_tags, $target_given_tags);
1170 $impl_tags = getImplicitTags ($expl_tags);
1171 }
1172
1173 // Take a list of user-supplied tag IDs to build a list of valid taginfo
1174 // records indexed by tag IDs (tag chain).
1175 function buildTagChainFromIds ($tagidlist)
1176 {
1177 global $taglist;
1178 $ret = array();
1179 foreach (array_unique ($tagidlist) as $tag_id)
1180 if (isset ($taglist[$tag_id]))
1181 $ret[] = $taglist[$tag_id];
1182 return $ret;
1183 }
1184
1185 // Process a given tag tree and return only meaningful branches. The resulting
1186 // (sub)tree will have refcnt leaves on every last branch.
1187 function getObjectiveTagTree ($tree, $realm, $preselect)
1188 {
1189 $self = __FUNCTION__;
1190 $ret = array();
1191 foreach ($tree as $taginfo)
1192 {
1193 $subsearch = $self ($taginfo['kids'], $realm, $preselect);
1194 // If the current node addresses something, add it to the result
1195 // regardless of how many sub-nodes it features.
1196 if
1197 (
1198 isset ($taginfo['refcnt'][$realm]) or
1199 count ($subsearch) > 1 or
1200 in_array ($taginfo['id'], $preselect)
1201 )
1202 $ret[] = array
1203 (
1204 'id' => $taginfo['id'],
1205 'tag' => $taginfo['tag'],
1206 'parent_id' => $taginfo['parent_id'],
1207 'refcnt' => $taginfo['refcnt'],
1208 'kids' => $subsearch
1209 );
1210 else
1211 $ret = array_merge ($ret, $subsearch);
1212 }
1213 return $ret;
1214 }
1215
1216 // Preprocess tag tree to get only tags which can effectively reduce given filter result,
1217 // than passes shrinked tag tree to getObjectiveTagTree and return its result.
1218 // This makes sense only if andor mode is 'and', otherwhise function does not modify tree.
1219 // 'Given filter' is a pair of $entity_list(filter result) and $preselect(filter data).
1220 // 'Effectively' means reduce to non-empty result.
1221 function getShrinkedTagTree($entity_list, $realm, $preselect) {
1222 global $tagtree;
1223 if ($preselect['andor'] != 'and' || empty($entity_list) && $preselect['is_empty'])
1224 return getObjectiveTagTree($tagtree, $realm, $preselect['tagidlist']);
1225
1226 $used_tags = array(); //associative, keys - tag ids, values - taginfos
1227 foreach ($entity_list as $entity)
1228 {
1229 foreach ($entity['etags'] as $etag)
1230 if (! array_key_exists($etag['id'], $used_tags))
1231 $used_tags[$etag['id']] = 1;
1232 else
1233 $used_tags[$etag['id']]++;
1234
1235 foreach ($entity['itags'] as $itag)
1236 if (! array_key_exists($itag['id'], $used_tags))
1237 $used_tags[$itag['id']] = 0;
1238 }
1239
1240 $shrinked_tree = shrinkSubtree($tagtree, $used_tags, $preselect, $realm);
1241 return getObjectiveTagTree($shrinked_tree, $realm, $preselect['tagidlist']);
1242 }
1243
1244 // deletes item from tag subtree unless it exists in $used_tags and not preselected
1245 function shrinkSubtree($tree, $used_tags, $preselect, $realm) {
1246 $self = __FUNCTION__;
1247
1248 foreach($tree as $i => &$item) {
1249 $item['kids'] = $self($item['kids'], $used_tags, $preselect, $realm);
1250 $item['kidc'] = count($item['kids']);
1251 if
1252 (
1253 ! array_key_exists($item['id'], $used_tags) &&
1254 ! in_array($item['id'], $preselect['tagidlist']) &&
1255 ! $item['kidc']
1256 )
1257 unset($tree[$i]);
1258 else {
1259 $item['refcnt'][$realm] = $used_tags[$item['id']];
1260 if (! $item['refcnt'][$realm])
1261 unset($item['refcnt'][$realm]);
1262 }
1263 }
1264 return $tree;
1265 }
1266
1267 // Get taginfo record by tag name, return NULL, if record doesn't exist.
1268 function getTagByName ($target_name)
1269 {
1270 global $taglist;
1271 foreach ($taglist as $taginfo)
1272 if ($taginfo['tag'] == $target_name)
1273 return $taginfo;
1274 return NULL;
1275 }
1276
1277 // Merge two chains, filtering dupes out. Return the resulting superset.
1278 function mergeTagChains ($chainA, $chainB)
1279 {
1280 // $ret = $chainA;
1281 // Reindex by tag id in any case.
1282 $ret = array();
1283 foreach ($chainA as $tag)
1284 $ret[$tag['id']] = $tag;
1285 foreach ($chainB as $tag)
1286 if (!isset ($ret[$tag['id']]))
1287 $ret[$tag['id']] = $tag;
1288 return $ret;
1289 }
1290
1291 function getCellFilter ()
1292 {
1293 global $sic;
1294 global $pageno;
1295 $staticFilter = getConfigVar ('STATIC_FILTER');
1296 if (isset ($_REQUEST['tagfilter']) and is_array ($_REQUEST['tagfilter']))
1297 {
1298 $_REQUEST['cft'] = $_REQUEST['tagfilter'];
1299 unset ($_REQUEST['tagfilter']);
1300 }
1301 $andor_used = FALSE;
1302 //if the page is submitted we get an andor value so we know they are trying to start a new filter or clearing the existing one.
1303 if(isset($_REQUEST['andor']))
1304 {
1305 $andor_used = TRUE;
1306 unset($_SESSION[$pageno]);
1307 }
1308 if (isset ($_SESSION[$pageno]['tagfilter']) and is_array ($_SESSION[$pageno]['tagfilter']) and !(isset($_REQUEST['cft'])) and $staticFilter == 'yes')
1309 {
1310 $_REQUEST['cft'] = $_SESSION[$pageno]['tagfilter'];
1311 }
1312 if (isset ($_SESSION[$pageno]['cfe']) and !(isset($sic['cfe'])) and $staticFilter == 'yes')
1313 {
1314 $sic['cfe'] = $_SESSION[$pageno]['cfe'];
1315 }
1316 if (isset ($_SESSION[$pageno]['andor']) and !(isset($_REQUEST['andor'])) and $staticFilter == 'yes')
1317 {
1318 $_REQUEST['andor'] = $_SESSION[$pageno]['andor'];
1319 }
1320
1321
1322 $ret = array
1323 (
1324 'tagidlist' => array(),
1325 'tnamelist' => array(),
1326 'pnamelist' => array(),
1327 'andor' => '',
1328 'text' => '',
1329 'extratext' => '',
1330 'expression' => array(),
1331 'urlextra' => '', // Just put text here and let makeHref call urlencode().
1332 'is_empty' => TRUE,
1333 );
1334 switch (TRUE)
1335 {
1336 case (!isset ($_REQUEST['andor'])):
1337 $andor2 = getConfigVar ('FILTER_DEFAULT_ANDOR');
1338 break;
1339 case ($_REQUEST['andor'] == 'and'):
1340 case ($_REQUEST['andor'] == 'or'):
1341 $_SESSION[$pageno]['andor'] = $_REQUEST['andor'];
1342 $ret['andor'] = $andor2 = $_REQUEST['andor'];
1343 break;
1344 default:
1345 showWarning ('Invalid and/or switch value in submitted form', __FUNCTION__);
1346 return NULL;
1347 }
1348 $andor1 = '';
1349 // Both tags and predicates, which don't exist, should be
1350 // handled somehow. Discard them silently for now.
1351 if (isset ($_REQUEST['cft']) and is_array ($_REQUEST['cft']))
1352 {
1353 $_SESSION[$pageno]['tagfilter'] = $_REQUEST['cft'];
1354 global $taglist;
1355 foreach ($_REQUEST['cft'] as $req_id)
1356 if (isset ($taglist[$req_id]))
1357 {
1358 $ret['tagidlist'][] = $req_id;
1359 $ret['tnamelist'][] = $taglist[$req_id]['tag'];
1360 $andor_used = $andor_used || (trim($andor1) != '');
1361 $ret['text'] .= $andor1 . '{' . $taglist[$req_id]['tag'] . '}';
1362 $andor1 = ' ' . $andor2 . ' ';
1363 $ret['urlextra'] .= '&cft[]=' . $req_id;
1364 }
1365 }
1366 if (isset ($_REQUEST['cfp']) and is_array ($_REQUEST['cfp']))
1367 {
1368 global $pTable;
1369 foreach ($_REQUEST['cfp'] as $req_name)
1370 if (isset ($pTable[$req_name]))
1371 {
1372 $ret['pnamelist'][] = $req_name;
1373 $andor_used = $andor_used || (trim($andor1) != '');
1374 $ret['text'] .= $andor1 . '[' . $req_name . ']';
1375 $andor1 = ' ' . $andor2 . ' ';
1376 $ret['urlextra'] .= '&cfp[]=' . $req_name;
1377 }
1378 }
1379 // Extra text comes from TEXTAREA and is easily screwed by standard escaping function.
1380 if (isset ($sic['cfe']))
1381 {
1382 $_SESSION[$pageno]['cfe'] = $sic['cfe'];
1383 // Only consider extra text, when it is a correct RackCode expression.
1384 $parse = spotPayload ($sic['cfe'], 'SYNT_EXPR');
1385 if ($parse['result'] == 'ACK')
1386 {
1387 $ret['extratext'] = trim ($sic['cfe']);
1388 $ret['urlextra'] .= '&cfe=' . $ret['extratext'];
1389 }
1390 }
1391 $finaltext = array();
1392 if (strlen ($ret['text']))
1393 $finaltext[] = '(' . $ret['text'] . ')';
1394 if (strlen ($ret['extratext']))
1395 $finaltext[] = '(' . $ret['extratext'] . ')';
1396 $andor_used = $andor_used || (count($finaltext) > 1);
1397 $finaltext = implode (' ' . $andor2 . ' ', $finaltext);
1398 if (strlen ($finaltext))
1399 {
1400 $ret['is_empty'] = FALSE;
1401 $parse = spotPayload ($finaltext, 'SYNT_EXPR');
1402 $ret['expression'] = $parse['result'] == 'ACK' ? $parse['load'] : NULL;
1403 // It's not quite fair enough to put the blame of the whole text onto
1404 // non-empty "extra" portion of it, but it's the only user-generated portion
1405 // of it, thus the most probable cause of parse error.
1406 if (strlen ($ret['extratext']))
1407 $ret['extraclass'] = $parse['result'] == 'ACK' ? 'validation-success' : 'validation-error';
1408 }
1409 if (! $andor_used)
1410 $ret['andor'] = getConfigVar ('FILTER_DEFAULT_ANDOR');
1411 else
1412 $ret['urlextra'] .= '&andor=' . $ret['andor'];
1413 return $ret;
1414 }
1415
1416 // Return an empty message log.
1417 function emptyLog ()
1418 {
1419 return array
1420 (
1421 'v' => 2,
1422 'm' => array()
1423 );
1424 }
1425
1426 // Return a message log consisting of only one message.
1427 function oneLiner ($code, $args = array())
1428 {
1429 $ret = emptyLog();
1430 $ret['m'][] = count ($args) ? array ('c' => $code, 'a' => $args) : array ('c' => $code);
1431 return $ret;
1432 }
1433
1434 // Merge message payload from two message logs given and return the result.
1435 function mergeLogs ($log1, $log2)
1436 {
1437 $ret = emptyLog();
1438 $ret['m'] = array_merge ($log1['m'], $log2['m']);
1439 return $ret;
1440 }
1441
1442 function validTagName ($s, $allow_autotag = FALSE)
1443 {
1444 if (1 == preg_match (TAGNAME_REGEXP, $s))
1445 return TRUE;
1446 if ($allow_autotag and 1 == preg_match (AUTOTAGNAME_REGEXP, $s))
1447 return TRUE;
1448 return FALSE;
1449 }
1450
1451 function redirectUser ($p, $t)
1452 {
1453 global $page;
1454 $l = "index.php?page=${p}&tab=${t}";
1455 if (isset ($page[$p]['bypass']) and isset ($_REQUEST[$page[$p]['bypass']]))
1456 $l .= '&' . $page[$p]['bypass'] . '=' . $_REQUEST[$page[$p]['bypass']];
1457 header ("Location: " . $l);
1458 die;
1459 }
1460
1461 function getRackCodeStats ()
1462 {
1463 global $rackCode;
1464 $defc = $grantc = $modc = 0;
1465 foreach ($rackCode as $s)
1466 switch ($s['type'])
1467 {
1468 case 'SYNT_DEFINITION':
1469 $defc++;
1470 break;
1471 case 'SYNT_GRANT':
1472 $grantc++;
1473 break;
1474 case 'SYNT_CTXMOD':
1475 $modc++;
1476 break;
1477 default:
1478 break;
1479 }
1480 $ret = array
1481 (
1482 'Definition sentences' => $defc,
1483 'Grant sentences' => $grantc,
1484 'Context mod sentences' => $modc
1485 );
1486 return $ret;
1487 }
1488
1489 function getRackImageWidth ()
1490 {
1491 global $rtwidth;
1492 return 3 + $rtwidth[0] + $rtwidth[1] + $rtwidth[2] + 3;
1493 }
1494
1495 function getRackImageHeight ($units)
1496 {
1497 return 3 + 3 + $units * 2;
1498 }
1499
1500 // Perform substitutions and return resulting string
1501 // used solely by buildLVSConfig()
1502 function apply_macros ($macros, $subject, &$error_macro_stat)
1503 {
1504 // clear all text before last %RESET% macro
1505 $reset_keyword = '%RESET%';
1506 $reset_position = mb_strpos($subject, $reset_keyword, 0);
1507 if ($reset_position === FALSE)
1508 $ret = $subject;
1509 else
1510 $ret = trim
1511 (
1512 mb_substr($subject, $reset_position + mb_strlen($reset_keyword)),
1513 "\n\r"
1514 );
1515
1516 foreach ($macros as $search => $replace)
1517 {
1518 if (empty($replace))
1519 {
1520 $replace = "<span class=\"msg_error\">$search</span>";
1521 $count = 0;
1522 $ret = str_replace ($search, $replace, $ret, $count);
1523 if ($count)
1524 {
1525 if (array_key_exists($search, $error_macro_stat))
1526 $error_macro_stat[$search] += $count;
1527 else
1528 $error_macro_stat[$search] = $count;
1529 }
1530 }
1531 else
1532 $ret = str_replace ($search, $replace, $ret);
1533 }
1534 return $ret;
1535 }
1536
1537 // throws RTBuildLVSConfigError exception if undefined macros found
1538 function buildLVSConfig ($object_id = 0)
1539 {
1540 if ($object_id <= 0)
1541 {
1542 showWarning ('Invalid argument', __FUNCTION__);
1543 return;
1544 }
1545 $oInfo = spotEntity ('object', $object_id);
1546 $defaults = getSLBDefaults (TRUE);
1547 $lbconfig = getSLBConfig ($object_id);
1548 if ($lbconfig === NULL)
1549 {
1550 showWarning ('getSLBConfig() failed', __FUNCTION__);
1551 return;
1552 }
1553 $newconfig = "#\n#\n# This configuration has been generated automatically by RackTables\n";
1554 $newconfig .= "# for object_id == ${object_id}\n# object name: ${oInfo['name']}\n#\n#\n\n\n";
1555
1556 $error_stat = array();
1557 foreach ($lbconfig as $vs_id => $vsinfo)
1558 {
1559 $newconfig .= "########################################################\n" .
1560 "# VS (id == ${vs_id}): " . (!strlen ($vsinfo['vs_name']) ? 'NO NAME' : $vsinfo['vs_name']) . "\n" .
1561 "# RS pool (id == ${vsinfo['pool_id']}): " . (!strlen ($vsinfo['pool_name']) ? 'ANONYMOUS' : $vsinfo['pool_name']) . "\n" .
1562 "########################################################\n";
1563 # The order of inheritance is: VS -> LB -> pool [ -> RS ]
1564 $macros = array
1565 (
1566 '%VIP%' => $vsinfo['vip'],
1567 '%VPORT%' => $vsinfo['vport'],
1568 '%PROTO%' => $vsinfo['proto'],
1569 '%VNAME%' => $vsinfo['vs_name'],
1570 '%RSPOOLNAME%' => $vsinfo['pool_name'],
1571 '%PRIO%' => $vsinfo['prio']
1572 );
1573 $newconfig .= "virtual_server ${vsinfo['vip']} ${vsinfo['vport']} {\n";
1574 $newconfig .= "\tprotocol ${vsinfo['proto']}\n";
1575 $newconfig .= lf_wrap (apply_macros
1576 (
1577 $macros,
1578 lf_wrap ($defaults['vs']) .
1579 lf_wrap ($vsinfo['vs_vsconfig']) .
1580 lf_wrap ($vsinfo['lb_vsconfig']) .
1581 lf_wrap ($vsinfo['pool_vsconfig']),
1582 $error_stat
1583 ));
1584 foreach ($vsinfo['rslist'] as $rs)
1585 {
1586 if (!strlen ($rs['rsport']))
1587 $rs['rsport'] = $vsinfo['vport'];
1588 $macros['%RSIP%'] = $rs['rsip'];
1589 $macros['%RSPORT%'] = $rs['rsport'];
1590 $newconfig .= "\treal_server ${rs['rsip']} ${rs['rsport']} {\n";
1591 $newconfig .= lf_wrap (apply_macros
1592 (
1593 $macros,
1594 lf_wrap ($defaults['rs']) .
1595 lf_wrap ($vsinfo['vs_rsconfig']) .
1596 lf_wrap ($vsinfo['lb_rsconfig']) .
1597 lf_wrap ($vsinfo['pool_rsconfig']) .
1598 lf_wrap ($rs['rs_rsconfig']),
1599 $error_stat
1600 ));
1601 $newconfig .= "\t}\n";
1602 }
1603 $newconfig .= "}\n\n\n";
1604 }
1605 if (! empty($error_stat))
1606 {
1607 $error_messages = array();
1608 foreach ($error_stat as $macro => $count)
1609 $error_messages[] = "Error: macro $macro can not be empty ($count occurences)";
1610 throw new RTBuildLVSConfigError($error_messages, $newconfig, $object_id);
1611 }
1612
1613 // FIXME: deal somehow with Mac-styled text, the below replacement will screw it up
1614 return dos2unix ($newconfig);
1615 }
1616
1617 // Indicate occupation state of each IP address: none, ordinary or problematic.
1618 function markupIPv4AddrList (&$addrlist)
1619 {
1620 foreach (array_keys ($addrlist) as $ip_bin)
1621 {
1622 $refc = array
1623 (
1624 'shared' => 0, // virtual
1625 'virtual' => 0, // loopback
1626 'regular' => 0, // connected host
1627 'router' => 0 // connected gateway
1628 );
1629 foreach ($addrlist[$ip_bin]['allocs'] as $a)
1630 $refc[$a['type']]++;
1631 $nvirtloopback = ($refc['shared'] + $refc['virtual'] > 0) ? 1 : 0; // modulus of virtual + shared
1632 $nreserved = ($addrlist[$ip_bin]['reserved'] == 'yes') ? 1 : 0; // only one reservation is possible ever
1633 $nrealms = $nreserved + $nvirtloopback + $refc['regular'] + $refc['router']; // latter two are connected and router allocations
1634
1635 if ($nrealms == 1)
1636 $addrlist[$ip_bin]['class'] = 'trbusy';
1637 elseif ($nrealms > 1)
1638 $addrlist[$ip_bin]['class'] = 'trerror';
1639 else
1640 $addrlist[$ip_bin]['class'] = '';
1641 }
1642 }
1643
1644 // Scan the given address list (returned by scanIPv4Space) and return a list of all routers found.
1645 function findRouters ($addrlist)
1646 {
1647 $ret = array();
1648 foreach ($addrlist as $addr)
1649 foreach ($addr['allocs'] as $alloc)
1650 if ($alloc['type'] == 'router')
1651 $ret[] = array
1652 (
1653 'id' => $alloc['object_id'],
1654 'iface' => $alloc['name'],
1655 'dname' => $alloc['object_name'],
1656 'addr' => $addr['ip']
1657 );
1658 return $ret;
1659 }
1660
1661 // Assist in tag chain sorting.
1662 function taginfoCmp ($tagA, $tagB)
1663 {
1664 return $tagA['ci'] - $tagB['ci'];
1665 }
1666
1667 // Compare networks. When sorting a tree, the records on the list will have
1668 // distinct base IP addresses.
1669 // "The comparison function must return an integer less than, equal to, or greater
1670 // than zero if the first argument is considered to be respectively less than,
1671 // equal to, or greater than the second." (c) PHP manual
1672 function IPv4NetworkCmp ($netA, $netB)
1673 {
1674 // On 64-bit systems this function can be reduced to just this:
1675 if (PHP_INT_SIZE == 8)
1676 return $netA['ip_bin'] - $netB['ip_bin'];
1677 // There's a problem just substracting one u32 integer from another,
1678 // because the result may happen big enough to become a negative i32
1679 // integer itself (PHP tries to cast everything it sees to signed int)
1680 // The comparison below must treat positive and negative values of both
1681 // arguments.
1682 // Equal values give instant decision regardless of their [equal] sign.
1683 if ($netA['ip_bin'] == $netB['ip_bin'])
1684 return 0;
1685 // Same-signed values compete arithmetically within one of i32 contiguous ranges:
1686 // 0x00000001~0x7fffffff 1~2147483647
1687 // 0 doesn't have any sign, and network 0.0.0.0 isn't allowed
1688 // 0x80000000~0xffffffff -2147483648~-1
1689 $signA = $netA['ip_bin'] / abs ($netA['ip_bin']);
1690 $signB = $netB['ip_bin'] / abs ($netB['ip_bin']);
1691 if ($signA == $signB)
1692 {
1693 if ($netA['ip_bin'] > $netB['ip_bin'])
1694 return 1;
1695 else
1696 return -1;
1697 }
1698 else // With only one of two values being negative, it... wins!
1699 {
1700 if ($netA['ip_bin'] < $netB['ip_bin'])
1701 return 1;
1702 else
1703 return -1;
1704 }
1705 }
1706
1707 // Modify the given tag tree so, that each level's items are sorted alphabetically.
1708 function sortTree (&$tree, $sortfunc = '')
1709 {
1710 if (!strlen ($sortfunc))
1711 return;
1712 $self = __FUNCTION__;
1713 usort ($tree, $sortfunc);
1714 // Don't make a mistake of directly iterating over the items of current level, because this way
1715 // the sorting will be performed on a _copy_ if each item, not the item itself.
1716 foreach (array_keys ($tree) as $tagid)
1717 $self ($tree[$tagid]['kids'], $sortfunc);
1718 }
1719
1720 function iptree_fill (&$netdata)
1721 {
1722 if (!isset ($netdata['kids']) or !count ($netdata['kids']))
1723 return;
1724 // If we really have nested prefixes, they must fit into the tree.
1725 $worktree = array
1726 (
1727 'ip_bin' => $netdata['ip_bin'],
1728 'mask' => $netdata['mask']
1729 );
1730 foreach ($netdata['kids'] as $pfx)
1731 iptree_embed ($worktree, $pfx);
1732 $netdata['kids'] = iptree_construct ($worktree);
1733 $netdata['kidc'] = count ($netdata['kids']);
1734 }
1735
1736 function iptree_construct ($node)
1737 {
1738 $self = __FUNCTION__;
1739
1740 if (!isset ($node['right']))
1741 {
1742 if (!isset ($node['ip']))
1743 {
1744 $node['ip'] = long2ip ($node['ip_bin']);
1745 $node['kids'] = array();
1746 $node['kidc'] = 0;
1747 $node['name'] = '';
1748 }
1749 return array ($node);
1750 }
1751 else
1752 return array_merge ($self ($node['left']), $self ($node['right']));
1753 }
1754
1755 function iptree_embed (&$node, $pfx)
1756 {
1757 $self = __FUNCTION__;
1758
1759 // hit?
1760 if ($node['ip_bin'] == $pfx['ip_bin'] and $node['mask'] == $pfx['mask'])
1761 {
1762 $node = $pfx;
1763 return;
1764 }
1765 if ($node['mask'] == $pfx['mask'])
1766 throw new RackTablesError ('the recurring loop lost control', RackTablesError::INTERNAL);
1767
1768 // split?
1769 if (!isset ($node['right']))
1770 {
1771 // Fill in db_first/db_last to make it possible to run scanIPv4Space() on the node.
1772 $node['left']['mask'] = $node['mask'] + 1;
1773 $node['left']['ip_bin'] = $node['ip_bin'];
1774 $node['left']['db_first'] = sprintf ('%u', $node['left']['ip_bin']);
1775 $node['left']['db_last'] = sprintf ('%u', $node['left']['ip_bin'] | binInvMaskFromDec ($node['left']['mask']));
1776
1777 $node['right']['mask'] = $node['mask'] + 1;
1778 $node['right']['ip_bin'] = $node['ip_bin'] + binInvMaskFromDec ($node['mask'] + 1) + 1;
1779 $node['right']['db_first'] = sprintf ('%u', $node['right']['ip_bin']);
1780 $node['right']['db_last'] = sprintf ('%u', $node['right']['ip_bin'] | binInvMaskFromDec ($node['right']['mask']));
1781 }
1782
1783 // repeat!
1784 if (($node['left']['ip_bin'] & binMaskFromDec ($node['left']['mask'])) == ($pfx['ip_bin'] & binMaskFromDec ($node['left']['mask'])))
1785 $self ($node['left'], $pfx);
1786 elseif (($node['right']['ip_bin'] & binMaskFromDec ($node['right']['mask'])) == ($pfx['ip_bin'] & binMaskFromDec ($node['left']['mask'])))
1787 $self ($node['right'], $pfx);
1788 else
1789 throw new RackTablesError ('cannot decide between left and right', RackTablesError::INTERNAL);
1790 }
1791
1792 function treeApplyFunc (&$tree, $func = '', $stopfunc = '')
1793 {
1794 if (!strlen ($func))
1795 return;
1796 $self = __FUNCTION__;
1797 foreach (array_keys ($tree) as $key)
1798 {
1799 $func ($tree[$key]);
1800 if (strlen ($stopfunc) and $stopfunc ($tree[$key]))
1801 continue;
1802 $self ($tree[$key]['kids'], $func);
1803 }
1804 }
1805
1806 function loadIPv4AddrList (&$netinfo)
1807 {
1808 loadOwnIPv4Addresses ($netinfo);
1809 markupIPv4AddrList ($netinfo['addrlist']);
1810 }
1811
1812 function countOwnIPv4Addresses (&$node)
1813 {
1814 $toscan = array();
1815 $node['addrt'] = 0;
1816 $node['mask_bin'] = binMaskFromDec ($node['mask']);
1817 $node['mask_bin_inv'] = binInvMaskFromDec ($node['mask']);
1818 $node['db_first'] = sprintf ('%u', 0x00000000 + $node['ip_bin'] & $node['mask_bin']);
1819 $node['db_last'] = sprintf ('%u', 0x00000000 + $node['ip_bin'] | ($node['mask_bin_inv']));
1820 if (empty ($node['kids']))
1821 {
1822 $toscan[] = array ('i32_first' => $node['db_first'], 'i32_last' => $node['db_last']);
1823 $node['addrt'] = binInvMaskFromDec ($node['mask']) + 1;
1824 }
1825 else
1826 foreach ($node['kids'] as $nested)
1827 if (!isset ($nested['id'])) // spare
1828 {
1829 $toscan[] = array ('i32_first' => $nested['db_first'], 'i32_last' => $nested['db_last']);
1830 $node['addrt'] += binInvMaskFromDec ($nested['mask']) + 1;
1831 }
1832 // Don't do anything more, because the displaying function will load the addresses anyway.
1833 return;
1834 $node['addrc'] = count (scanIPv4Space ($toscan));
1835 }
1836
1837 function nodeIsCollapsed ($node)
1838 {
1839 return $node['symbol'] == 'node-collapsed';
1840 }
1841
1842 function loadOwnIPv4Addresses (&$node)
1843 {
1844 $toscan = array();
1845 if (!isset ($node['kids']) or !count ($node['kids']))
1846 $toscan[] = array ('i32_first' => $node['db_first'], 'i32_last' => $node['db_last']);
1847 else
1848 foreach ($node['kids'] as $nested)
1849 if (!isset ($nested['id'])) // spare
1850 $toscan[] = array ('i32_first' => $nested['db_first'], 'i32_last' => $nested['db_last']);
1851 $node['addrlist'] = scanIPv4Space ($toscan);
1852 $node['addrc'] = count ($node['addrlist']);
1853 }
1854
1855 function prepareIPv4Tree ($netlist, $expanded_id = 0)
1856 {
1857 // treeFromList() requires parent_id to be correct for an item to get onto the tree,
1858 // so perform necessary pre-processing to make orphans belong to root. This trick
1859 // was earlier performed by getIPv4NetworkList().
1860 $netids = array_keys ($netlist);
1861 foreach ($netids as $cid)
1862 if (!in_array ($netlist[$cid]['parent_id'], $netids))
1863 $netlist[$cid]['parent_id'] = NULL;
1864 $tree = treeFromList ($netlist); // medium call
1865 sortTree ($tree, 'IPv4NetworkCmp');
1866 // complement the tree before markup to make the spare networks have "symbol" set
1867 treeApplyFunc ($tree, 'iptree_fill');
1868 iptree_markup_collapsion ($tree, getConfigVar ('TREE_THRESHOLD'), $expanded_id);
1869 // count addresses after the markup to skip computation for hidden tree nodes
1870 treeApplyFunc ($tree, 'countOwnIPv4Addresses', 'nodeIsCollapsed');
1871 return $tree;
1872 }
1873
1874 // Check all items of the tree recursively, until the requested target id is
1875 // found. Mark all items leading to this item as "expanded", collapsing all
1876 // the rest, which exceed the given threshold (if the threshold is given).
1877 function iptree_markup_collapsion (&$tree, $threshold = 1024, $target = 0)
1878 {
1879 $self = __FUNCTION__;
1880 $ret = FALSE;
1881 foreach (array_keys ($tree) as $key)
1882 {
1883 $here = ($target === 'ALL' or ($target > 0 and isset ($tree[$key]['id']) and $tree[$key]['id'] == $target));
1884 $below = $self ($tree[$key]['kids'], $threshold, $target);
1885 if (!$tree[$key]['kidc']) // terminal node
1886 $tree[$key]['symbol'] = 'spacer';
1887 elseif ($tree[$key]['kidc'] < $threshold)
1888 $tree[$key]['symbol'] = 'node-expanded-static';
1889 elseif ($here or $below)
1890 $tree[$key]['symbol'] = 'node-expanded';
1891 else
1892 $tree[$key]['symbol'] = 'node-collapsed';
1893 $ret = ($ret or $here or $below); // parentheses are necessary for this to be computed correctly
1894 }
1895 return $ret;
1896 }
1897
1898 // Convert entity name to human-readable value
1899 function formatEntityName ($name) {
1900 switch ($name)
1901 {
1902 case 'ipv4net':
1903 return 'IPv4 Network';
1904 case 'ipv4rspool':
1905 return 'IPv4 RS Pool';
1906 case 'ipv4vs':
1907 return 'IPv4 Virtual Service';
1908 case 'object':
1909 return 'Object';
1910 case 'rack':
1911 return 'Rack';
1912 case 'user':
1913 return 'User';
1914 }
1915 return 'invalid';
1916 }
1917
1918 // Take a MySQL or other generic timestamp and make it prettier
1919 function formatTimestamp ($timestamp) {
1920 return date('n/j/y g:iA', strtotime($timestamp));
1921 }
1922
1923 // Display hrefs for all of a file's parents. If scissors are requested,
1924 // prepend cutting button to each of them.
1925 function serializeFileLinks ($links, $scissors = FALSE)
1926 {
1927 $comma = '';
1928 $ret = '';
1929 foreach ($links as $link_id => $li)
1930 {
1931 switch ($li['entity_type'])
1932 {
1933 case 'ipv4net':
1934 $params = "page=ipv4net&id=";
1935 break;
1936 case 'ipv4rspool':
1937 $params = "page=ipv4rspool&pool_id=";
1938 break;
1939 case 'ipv4vs':
1940 $params = "page=ipv4vs&vs_id=";
1941 break;
1942 case 'object':
1943 $params = "page=object&object_id=";
1944 break;
1945 case 'rack':
1946 $params = "page=rack&rack_id=";
1947 break;
1948 case 'user':
1949 $params = "page=user&user_id=";
1950 break;
1951 }
1952 $ret .= $comma;
1953 if ($scissors)
1954 {
1955 $ret .= "<a href='" . makeHrefProcess(array('op'=>'unlinkFile', 'link_id'=>$link_id)) . "'";
1956 $ret .= getImageHREF ('cut') . '</a> ';
1957 }
1958 $ret .= sprintf("<a href='index.php?%s%s'>%s</a>", $params, $li['entity_id'], $li['name']);
1959 $comma = '<br>';
1960 }
1961 return $ret;
1962 }
1963
1964 // Convert filesize to appropriate unit and make it human-readable
1965 function formatFileSize ($bytes) {
1966 // bytes
1967 if($bytes < 1024) // bytes
1968 return "${bytes} bytes";
1969
1970 // kilobytes
1971 if ($bytes < 1024000)
1972 return sprintf ("%.1fk", round (($bytes / 1024), 1));
1973
1974 // megabytes
1975 return sprintf ("%.1f MB", round (($bytes / 1024000), 1));
1976 }
1977
1978 // Reverse of formatFileSize, it converts human-readable value to bytes
1979 function convertToBytes ($value) {
1980 $value = trim($value);
1981 $last = strtolower($value[strlen($value)-1]);
1982 switch ($last)
1983 {
1984 case 'g':
1985 $value *= 1024;
1986 case 'm':
1987 $value *= 1024;
1988 case 'k':
1989 $value *= 1024;
1990 }
1991
1992 return $value;
1993 }
1994
1995 function ip_quad2long ($ip)
1996 {
1997 return sprintf("%u", ip2long($ip));
1998 }
1999
2000 function ip_long2quad ($quad)
2001 {
2002 return long2ip($quad);
2003 }
2004
2005 function makeHref($params = array())
2006 {
2007 $ret = 'index.php?';
2008 $first = true;
2009 foreach($params as $key=>$value)
2010 {
2011 if (!$first)
2012 $ret.='&';
2013 $ret .= urlencode($key).'='.urlencode($value);
2014 $first = false;
2015 }
2016 return $ret;
2017 }
2018
2019 function makeHrefProcess($params = array())
2020 {
2021 global $pageno, $tabno;
2022 $ret = 'process.php?';
2023 $first = true;
2024 if (!isset($params['page']))
2025 $params['page'] = $pageno;
2026 if (!isset($params['tab']))
2027 $params['tab'] = $tabno;
2028 foreach($params as $key=>$value)
2029 {
2030 if (!$first)
2031 $ret.='&';
2032 $ret .= urlencode($key).'='.urlencode($value);
2033 $first = false;
2034 }
2035 return $ret;
2036 }
2037
2038 function makeHrefForHelper ($helper_name, $params = array())
2039 {
2040 $ret = 'popup.php?helper=' . $helper_name;
2041 foreach($params as $key=>$value)
2042 $ret .= '&'.urlencode($key).'='.urlencode($value);
2043 return $ret;
2044 }
2045
2046 // Process the given list of records to build data suitable for printNiftySelect()
2047 // (like it was formerly executed by printSelect()). Screen out vendors according
2048 // to VENDOR_SIEVE, if object type ID is provided. However, the OPTGROUP with already
2049 // selected OPTION is protected from being screened.
2050 function cookOptgroups ($recordList, $object_type_id = 0, $existing_value = 0)
2051 {
2052 $ret = array();
2053 // Always keep "other" OPTGROUP at the SELECT bottom.
2054 $therest = array();
2055 foreach ($recordList as $dict_key => $dict_value)
2056 if (strpos ($dict_value, '%GSKIP%') !== FALSE)
2057 {
2058 $tmp = explode ('%GSKIP%', $dict_value, 2);
2059 $ret[$tmp[0]][$dict_key] = $tmp[1];
2060 }
2061 elseif (strpos ($dict_value, '%GPASS%') !== FALSE)
2062 {
2063 $tmp = explode ('%GPASS%', $dict_value, 2);
2064 $ret[$tmp[0]][$dict_key] = $tmp[1];
2065 }
2066 else
2067 $therest[$dict_key] = $dict_value;
2068 if ($object_type_id != 0)
2069 {
2070 $screenlist = array();
2071 foreach (explode (';', getConfigVar ('VENDOR_SIEVE')) as $sieve)
2072 if (preg_match ("/^([^@]+)(@${object_type_id})?\$/", trim ($sieve), $regs)){
2073 $screenlist[] = $regs[1];
2074 }
2075 foreach (array_keys ($ret) as $vendor)
2076 if (in_array ($vendor, $screenlist))
2077 {
2078 $ok_to_screen = TRUE;
2079 if ($existing_value)
2080 foreach (array_keys ($ret[$vendor]) as $recordkey)
2081 if ($recordkey == $existing_value)
2082 {
2083 $ok_to_screen = FALSE;
2084 break;
2085 }
2086 if ($ok_to_screen)
2087 unset ($ret[$vendor]);
2088 }
2089 }
2090 $ret['other'] = $therest;
2091 return $ret;
2092 }
2093
2094 function dos2unix ($text)
2095 {
2096 return str_replace ("\r\n", "\n", $text);
2097 }
2098
2099 function unix2dos ($text)
2100 {
2101 return str_replace ("\n", "\r\n", $text);
2102 }
2103
2104 function buildPredicateTable ($parsetree)
2105 {
2106 $ret = array();
2107 foreach ($parsetree as $sentence)
2108 if ($sentence['type'] == 'SYNT_DEFINITION')
2109 $ret[$sentence['term']] = $sentence['definition'];
2110 // Now we have predicate table filled in with the latest definitions of each
2111 // particular predicate met. This isn't as chik, as on-the-fly predicate
2112 // overloading during allow/deny scan, but quite sufficient for this task.
2113 return $ret;
2114 }
2115
2116 // Take a list of records and filter against given RackCode expression. Return
2117 // the original list intact, if there was no filter requested, but return an
2118 // empty list, if there was an error.
2119 function filterCellList ($list_in, $expression = array())
2120 {
2121 if ($expression === NULL)
2122 return array();
2123 if (!count ($expression))
2124 return $list_in;
2125 $list_out = array();
2126 foreach ($list_in as $item_key => $item_value)
2127 if (TRUE === judgeCell ($item_value, $expression))
2128 $list_out[$item_key] = $item_value;
2129 return $list_out;
2130 }
2131
2132 // Tell, if the given expression is true for the given entity. Take complete record on input.
2133 function judgeCell ($cell, $expression)
2134 {
2135 global $pTable;
2136 return eval_expression
2137 (
2138 $expression,
2139 array_merge
2140 (
2141 $cell['etags'],
2142 $cell['itags'],
2143 $cell['atags']
2144 ),
2145 $pTable,
2146 TRUE
2147 );
2148 }
2149
2150 // Tell, if a constraint from config option permits given record.
2151 function considerConfiguredConstraint ($cell, $varname)
2152 {
2153 if (!strlen (getConfigVar ($varname)))
2154 return TRUE; // no restriction
2155 global $parseCache;
2156 if (!isset ($parseCache[$varname]))
2157 // getConfigVar() doesn't re-read the value from DB because of its
2158 // own cache, so there is no race condition here between two calls.
2159 $parseCache[$varname] = spotPayload (getConfigVar ($varname), 'SYNT_EXPR');
2160 if ($parseCache[$varname]['result'] != 'ACK')
2161 return FALSE; // constraint set, but cannot be used due to compilation error
2162 return judgeCell ($cell, $parseCache[$varname]['load']);
2163 }
2164
2165 // Return list of records in the given realm, which conform to
2166 // the given RackCode expression. If the realm is unknown or text
2167 // doesn't validate as a RackCode expression, return NULL.
2168 // Otherwise (successful scan) return a list of all matched
2169 // records, even if the list is empty (array() !== NULL). If the
2170 // text is an empty string, return all found records in the given
2171 // realm.
2172 function scanRealmByText ($realm = NULL, $ftext = '')
2173 {
2174 switch ($realm)
2175 {
2176 case 'object':
2177 case 'user':
2178 case 'ipv4net':
2179 case 'file':
2180 case 'ipv4vs':
2181 case 'ipv4rspool':
2182 if (!strlen ($ftext = trim ($ftext)))
2183 $fexpr = array();
2184 else
2185 {
2186 $fparse = spotPayload ($ftext, 'SYNT_EXPR');
2187 if ($fparse['result'] != 'ACK')
2188 return NULL;
2189 $fexpr = $fparse['load'];
2190 }
2191 return filterCellList (listCells ($realm), $fexpr);
2192 default:
2193 throw new InvalidArgException ('$realm', $realm);
2194 }
2195
2196 }
2197
2198 function getIPv4VSOptions ()
2199 {
2200 $ret = array();
2201 foreach (listCells ('ipv4vs') as $vsid => $vsinfo)
2202 $ret[$vsid] = $vsinfo['dname'] . (!strlen ($vsinfo['name']) ? '' : " (${vsinfo['name']})");
2203 return $ret;
2204 }
2205
2206 function getIPv4RSPoolOptions ()
2207 {
2208 $ret = array();
2209 foreach (listCells ('ipv4rspool') as $pool_id => $poolInfo)
2210 $ret[$pool_id] = $poolInfo['name'];
2211 return $ret;
2212 }
2213
2214 // Derive a complete cell structure from the given username regardless
2215 // if it is a local account or not.
2216 function constructUserCell ($username)
2217 {
2218 if (NULL !== ($userid = getUserIDByUsername ($username)))
2219 return spotEntity ('user', $userid);
2220 $ret = array
2221 (
2222 'realm' => 'user',
2223 'user_name' => $username,
2224 'user_realname' => '',
2225 'etags' => array(),
2226 'itags' => array(),
2227 );
2228 $ret['atags'] = generateEntityAutoTags ($ret);
2229 return $ret;
2230 }
2231
2232 // Let's have this debug helper here to enable debugging of process.php w/o interface.php.
2233 function dump ($var)
2234 {
2235 echo '<div align=left><pre>';
2236 print_r ($var);
2237 echo '</pre></div>';
2238 }
2239
2240 function getTagChart ($limit = 0, $realm = 'total', $special_tags = array())
2241 {
2242 global $taglist;
2243 // first build top-N chart...
2244 $toplist = array();
2245 foreach ($taglist as $taginfo)
2246 if (isset ($taginfo['refcnt'][$realm]))
2247 $toplist[$taginfo['id']] = $taginfo['refcnt'][$realm];
2248 arsort ($toplist, SORT_NUMERIC);
2249 $ret = array();
2250 $done = 0;
2251 foreach (array_keys ($toplist) as $tag_id)
2252 {
2253 $ret[$tag_id] = $taglist[$tag_id];
2254 if (++$done == $limit)
2255 break;
2256 }
2257 // ...then make sure, that every item of the special list is shown
2258 // (using the same sort order)
2259 $extra = array();
2260 foreach ($special_tags as $taginfo)
2261 if (!array_key_exists ($taginfo['id'], $ret))
2262 $extra[$taginfo['id']] = $taglist[$taginfo['id']]['refcnt'][$realm];
2263 arsort ($extra, SORT_NUMERIC);
2264 foreach (array_keys ($extra) as $tag_id)
2265 $ret[] = $taglist[$tag_id];
2266 return $ret;
2267 }
2268
2269 function decodeObjectType ($objtype_id, $style = 'r')
2270 {
2271 static $types = array();
2272 if (!count ($types))
2273 $types = array
2274 (
2275 'r' => readChapter (CHAP_OBJTYPE),
2276 'a' => readChapter (CHAP_OBJTYPE, 'a'),
2277 'o' => readChapter (CHAP_OBJTYPE, 'o')
2278 );
2279 return $types[$style][$objtype_id];
2280 }
2281
2282 function isolatedPermission ($p, $t, $cell)
2283 {
2284 // This function is called from both "file" page and a number of other pages,
2285 // which have already fixed security context and authorized the user for it.
2286 // OTOH, it is necessary here to authorize against the current file, which
2287 // means saving the current context and building a new one.
2288 global
2289 $expl_tags,
2290 $impl_tags,
2291 $target_given_tags,
2292 $auto_tags;
2293 // push current context
2294 $orig_expl_tags = $expl_tags;
2295 $orig_impl_tags = $impl_tags;
2296 $orig_target_given_tags = $target_given_tags;
2297 $orig_auto_tags = $auto_tags;
2298 // retarget
2299 fixContext ($cell);
2300 // remember decision
2301 $ret = permitted ($p, $t);
2302 // pop context
2303 $expl_tags = $orig_expl_tags;
2304 $impl_tags = $orig_impl_tags;
2305 $target_given_tags = $orig_target_given_tags;
2306 $auto_tags = $orig_auto_tags;
2307 return $ret;
2308 }
2309
2310 function getPortListPrefs()
2311 {
2312 $ret = array();
2313 if (0 >= ($ret['iif_pick'] = getConfigVar ('DEFAULT_PORT_IIF_ID')))
2314 $ret['iif_pick'] = 1;
2315 $ret['oif_picks'] = array();
2316 foreach (explode (';', getConfigVar ('DEFAULT_PORT_OIF_IDS')) as $tmp)
2317 {
2318 $tmp = explode ('=', trim ($tmp));
2319 if (count ($tmp) == 2 and $tmp[0] > 0 and $tmp[1] > 0)
2320 $ret['oif_picks'][$tmp[0]] = $tmp[1];
2321 }
2322 // enforce default value
2323 if (!array_key_exists (1, $ret['oif_picks']))
2324 $ret['oif_picks'][1] = 24;
2325 $ret['selected'] = $ret['iif_pick'] . '-' . $ret['oif_picks'][$ret['iif_pick']];
2326 return $ret;
2327 }
2328
2329 // Return data for printNiftySelect() with port type options. All OIF options
2330 // for the default IIF will be shown, but only the default OIFs will be present
2331 // for each other IIFs. IIFs, for which there is no default OIF, will not
2332 // be listed.
2333 // This SELECT will be used for the "add new port" form.
2334 function getNewPortTypeOptions()
2335 {
2336 $ret = array();
2337 $prefs = getPortListPrefs();
2338 foreach (getPortInterfaceCompat() as $row)
2339 {
2340 if ($row['iif_id'] == $prefs['iif_pick'])
2341 $optgroup = $row['iif_name'];
2342 elseif (array_key_exists ($row['iif_id'], $prefs['oif_picks']) and $prefs['oif_picks'][$row['iif_id']] == $row['oif_id'])
2343 $optgroup = 'other';
2344 else
2345 continue;
2346 if (!array_key_exists ($optgroup, $ret))
2347 $ret[$optgroup] = array();
2348 $ret[$optgroup][$row['iif_id'] . '-' . $row['oif_id']] = $row['oif_name'];
2349 }
2350 return $ret;
2351 }
2352
2353 // Return a serialized version of VLAN configuration for a port.
2354 // If a native VLAN is defined, print it first. All other VLANs
2355 // are tagged and are listed after a plus sign. When no configuration
2356 // is set for a port, return "default" string.
2357 function serializeVLANPack ($vlanport)
2358 {
2359 if (!array_key_exists ('mode', $vlanport))
2360 return 'error';
2361 switch ($vlanport['mode'])
2362 {
2363 case 'none':
2364 return 'none';
2365 case 'access':
2366 $ret = 'A';
2367 break;
2368 case 'trunk':
2369 $ret = 'T';
2370 break;
2371 case 'uplink':
2372 $ret = 'U';
2373 break;
2374 case 'downlink':
2375 $ret = 'D';
2376 break;
2377 default:
2378 return 'error';
2379 }
2380 $tagged = array();
2381 foreach ($vlanport['allowed'] as $vlan_id)
2382 if ($vlan_id != $vlanport['native'])
2383 $tagged[] = $vlan_id;
2384 sort ($tagged);
2385 $ret .= $vlanport['native'] ? $vlanport['native'] : '';
2386 $tagged_bits = array();
2387 $id_from = $id_to = 0;
2388 foreach ($tagged as $next_id)
2389 {
2390 if ($id_to)
2391 {
2392 if ($next_id == $id_to + 1) // merge
2393 {
2394 $id_to = $next_id;
2395 continue;
2396 }
2397 // flush
2398 $tagged_bits[] = $id_from == $id_to ? $id_from : "${id_from}-${id_to}";
2399 }
2400 $id_from = $id_to = $next_id; // start next pair
2401 }
2402 // pull last pair
2403 if ($id_to)
2404 $tagged_bits[] = $id_from == $id_to ? $id_from : "${id_from}-${id_to}";
2405 if (count ($tagged))
2406 $ret .= '+' . implode (', ', $tagged_bits);
2407 return strlen ($ret) ? $ret : 'default';
2408 }
2409
2410 // Decode VLAN compound key (which is a string formatted DOMAINID-VLANID) and
2411 // return the numbers as an array of two.
2412 function decodeVLANCK ($string)
2413 {
2414 $matches = array();
2415 if (1 != preg_match ('/^([[:digit:]]+)-([[:digit:]]+)$/', $string, $matches))
2416 throw new InvalidArgException ('VLAN compound key', $string);
2417 return array ($matches[1], $matches[2]);
2418 }
2419
2420 // Return VLAN name formatted for HTML output (note, that input
2421 // argument comes from database unescaped).
2422 function formatVLANName ($vlaninfo, $context = 'markup long')
2423 {
2424 switch ($context)
2425 {
2426 case 'option':
2427 $ret = $vlaninfo['vlan_id'];
2428 if ($vlaninfo['vlan_descr'] != '')
2429 $ret .= ' ' . niftyString ($vlaninfo['vlan_descr']);
2430 return $ret;
2431 case 'label':
2432 $ret = $vlaninfo['vlan_id'];
2433 if ($vlaninfo['vlan_descr'] != '')
2434 $ret .= ' <i>(' . niftyString ($vlaninfo['vlan_descr']) . ')</i>';
2435 return $ret;
2436 case 'plain long':
2437 $ret = 'VLAN' . $vlaninfo['vlan_id'];
2438 if ($vlaninfo['vlan_descr'] != '')
2439 $ret .= ' (' . niftyString ($vlaninfo['vlan_descr'], 20, FALSE) . ')';
2440 return $ret;
2441 case 'hyperlink':
2442 $ret = '<a href="';
2443 $ret .= makeHref (array ('page' => 'vlan', 'vlan_ck' => $vlaninfo['domain_id'] . '-' . $vlaninfo['vlan_id']));
2444 $ret .= '">' . formatVLANName ($vlaninfo, 'markup long') . '</a>';
2445 return $ret;
2446 case 'markup long':
2447 default:
2448 $ret = 'VLAN' . $vlaninfo['vlan_id'];
2449 $ret .= ' @' . niftyString ($vlaninfo['domain_descr']);
2450 if ($vlaninfo['vlan_descr'] != '')
2451 $ret .= ' <i>(' . niftyString ($vlaninfo['vlan_descr']) . ')</i>';
2452 return $ret;
2453 }
2454 }
2455
2456 // map interface name
2457 function ios12ShortenIfName ($ifname)
2458 {
2459 $ifname = preg_replace ('@^Ethernet(.+)$@', 'e\\1', $ifname);
2460 $ifname = preg_replace ('@^FastEthernet(.+)$@', 'fa\\1', $ifname);
2461 $ifname = preg_replace ('@^GigabitEthernet(.+)$@', 'gi\\1', $ifname);
2462 $ifname = preg_replace ('@^TenGigabitEthernet(.+)$@', 'te\\1', $ifname);
2463 $ifname = preg_replace ('@^Port-channel(.+)$@', 'po\\1', $ifname);
2464 $ifname = preg_replace ('@^XGigabitEthernet(.+)$@', 'xg\\1', $ifname);
2465 $ifname = strtolower ($ifname);
2466 return $ifname;
2467 }
2468
2469 function iosParseVLANString ($string)
2470 {
2471 $ret = array();
2472 foreach (explode (',', $string) as $item)
2473 {
2474 $matches = array();
2475 $item = trim ($item, ' ');
2476 if (preg_match ('/^([[:digit:]]+)$/', $item, $matches))
2477 $ret[] = $matches[1];
2478 elseif (preg_match ('/^([[:digit:]]+)-([[:digit:]]+)$/', $item, $matches))
2479 $ret = array_merge ($ret, range ($matches[1], $matches[2]));
2480 }
2481 return $ret;
2482 }
2483
2484 // Scan given array and return the key, which addresses the first item
2485 // with requested column set to given value (or NULL if there is none such).
2486 // Note that 0 and NULL mean completely different things and thus
2487 // require strict checking (=== and !===).
2488 function scanArrayForItem ($table, $scan_column, $scan_value)
2489 {
2490 foreach ($table as $key => $row)
2491 if ($row[$scan_column] == $scan_value)
2492 return $key;
2493 return NULL;
2494 }
2495
2496 // Return TRUE, if every value of A1 is present in A2 and vice versa,
2497 // regardless of each array's sort order and indexing.
2498 function array_values_same ($a1, $a2)
2499 {
2500 return !count (array_diff ($a1, $a2)) and !count (array_diff ($a2, $a1));
2501 }
2502
2503 // Use the VLAN switch template to set VST role for each port of
2504 // the provided list. Return resulting list.
2505 function apply8021QOrder ($vst_id, $portlist)
2506 {
2507 $vst = getVLANSwitchTemplate ($vst_id);
2508 foreach (array_keys ($portlist) as $port_name)
2509 {
2510 foreach ($vst['rules'] as $rule)
2511 if (preg_match ($rule['port_pcre'], $port_name))
2512 {
2513 $portlist[$port_name]['vst_role'] = $rule['port_role'];
2514 $portlist[$port_name]['wrt_vlans'] = buildVLANFilter ($rule['port_role'], $rule['wrt_vlans']);
2515 continue 2;
2516 }
2517 $portlist[$port_name]['vst_role'] = 'none';
2518 }
2519 return $portlist;
2520 }
2521
2522 // return a sequence of ranges for given string form and port role
2523 function buildVLANFilter ($role, $string)
2524 {
2525 // set base
2526 switch ($role)
2527 {
2528 case 'access': // 1-4094
2529 $min = VLAN_MIN_ID;
2530 $max = VLAN_MAX_ID;
2531 break;
2532 case 'trunk': // 2-4094
2533 case 'uplink':
2534 case 'downlink':
2535 case 'anymode':
2536 $min = VLAN_MIN_ID + 1;
2537 $max = VLAN_MAX_ID;
2538 break;
2539 default: // none
2540 return array();
2541 }
2542 if ($string == '') // fast track
2543 return array (array ('from' => $min, 'to' => $max));
2544 // transform
2545 $vlanidlist = array();
2546 foreach (iosParseVLANString ($string) as $vlan_id)
2547 if ($min <= $vlan_id and $vlan_id <= $max)
2548 $vlanidlist[] = $vlan_id;
2549 return listToRanges ($vlanidlist);
2550 }
2551
2552 // pack set of integers into list of integer ranges
2553 // e.g. (1, 2, 3, 5, 6, 7, 9, 11) => ((1, 3), (5, 7), (9, 9), (11, 11))
2554 // The second argument, when it is different from 0, limits amount of
2555 // items in each generated range.
2556 function listToRanges ($vlanidlist, $limit = 0)
2557 {
2558 sort ($vlanidlist);
2559 $ret = array();
2560 $from = $to = NULL;
2561 foreach ($vlanidlist as $vlan_id)
2562 if ($from == NULL)
2563 {
2564 if ($limit == 1)
2565 $ret[] = array ('from' => $vlan_id, 'to' => $vlan_id);
2566 else
2567 $from = $to = $vlan_id;
2568 }
2569 elseif ($to + 1 == $vlan_id)
2570 {
2571 $to = $vlan_id;
2572 if ($to - $from + 1 == $limit)
2573 {
2574 // cut accumulated range and start over
2575 $ret[] = array ('from' => $from, 'to' => $to);
2576 $from = $to = NULL;
2577 }
2578 }
2579 else
2580 {
2581 $ret[] = array ('from' => $from, 'to' => $to);
2582 $from = $to = $vlan_id;
2583 }
2584 if ($from != NULL)
2585 $ret[] = array ('from' => $from, 'to' => $to);
2586 return $ret;
2587 }
2588
2589 // return TRUE, if given VLAN ID belongs to one of filter's ranges
2590 function matchVLANFilter ($vlan_id, $vfilter)
2591 {
2592 foreach ($vfilter as $range)
2593 if ($range['from'] <= $vlan_id and $vlan_id <= $range['to'])
2594 return TRUE;
2595 return FALSE;
2596 }
2597
2598 function exportSwitch8021QConfig
2599 (
2600 $vswitch,
2601 $device_vlanlist,
2602 $before,
2603 $changes
2604 )
2605 {
2606 // only ignore VLANs, which exist and are explicitly shown as "alien"
2607 $old_managed_vlans = array();
2608 $domain_vlanlist = getDomainVLANs ($vswitch['domain_id']);
2609 foreach ($device_vlanlist as $vlan_id)
2610 if
2611 (
2612 !array_key_exists ($vlan_id, $domain_vlanlist) or
2613 $domain_vlanlist[$vlan_id]['vlan_type'] != 'alien'
2614 )
2615 $old_managed_vlans[] = $vlan_id;
2616 $ports_to_do = array();
2617 $after = $before;
2618 foreach ($changes as $port_name => $port)
2619 {
2620 $ports_to_do[$port_name] = array
2621 (
2622 'old_mode' => $before[$port_name]['mode'],
2623 'old_allowed' => $before[$port_name]['allowed'],
2624 'old_native' => $before[$port_name]['native'],
2625 'new_mode' => $port['mode'],
2626 'new_allowed' => $port['allowed'],
2627 'new_native' => $port['native'],
2628 );
2629 $after[$port_name] = $port;
2630 }
2631 // New VLAN table is a union of:
2632 // 1. all compulsory VLANs
2633 // 2. all "current" non-alien allowed VLANs of those ports, which are left
2634 // intact (regardless if a VLAN exists in VLAN domain, but looking,
2635 // if it is present in device's own VLAN table)
2636 // 3. all "new" allowed VLANs of those ports, which we do "push" now
2637 // Like for old_managed_vlans, a VLANs is never listed, only if it
2638 // exists and belongs to "alien" type.
2639 $new_managed_vlans = array();
2640 // 1
2641 foreach ($domain_vlanlist as $vlan_id => $vlan)
2642 if ($vlan['vlan_type'] == 'compulsory')
2643 $new_managed_vlans[] = $vlan_id;
2644 // 2
2645 foreach ($before as $port_name => $port)
2646 if (!array_key_exists ($port_name, $changes))
2647 foreach ($port['allowed'] as $vlan_id)
2648 {
2649 if (in_array ($vlan_id, $new_managed_vlans))
2650 continue;
2651 if
2652 (
2653 array_key_exists ($vlan_id, $domain_vlanlist) and
2654 $domain_vlanlist[$vlan_id]['vlan_type'] == 'alien'
2655 )
2656 continue;
2657 if (in_array ($vlan_id, $device_vlanlist))
2658 $new_managed_vlans[] = $vlan_id;
2659 }
2660 // 3
2661 foreach ($changes as $port)
2662 foreach ($port['allowed'] as $vlan_id)
2663 if
2664 (
2665 $domain_vlanlist[$vlan_id]['vlan_type'] == 'ondemand' and
2666 !in_array ($vlan_id, $new_managed_vlans)
2667 )
2668 $new_managed_vlans[] = $vlan_id;
2669 $crq = array();
2670 // Before removing each old VLAN as such it is necessary to unassign
2671 // ports from it (to remove VLAN from each ports' list of "allowed"
2672 // VLANs). This change in turn requires, that a port's "native"
2673 // VLAN isn't set to the one being removed from its "allowed" list.
2674 foreach ($ports_to_do as $port_name => $port)
2675 switch ($port['old_mode'] . '->' . $port['new_mode'])
2676 {
2677 case 'trunk->trunk':
2678 // "old" native is set and differs from the "new" native
2679 if ($port['old_native'] and $port['old_native'] != $port['new_native'])
2680 $crq[] = array
2681 (
2682 'opcode' => 'unset native',
2683 'arg1' => $port_name,
2684 'arg2' => $port['old_native'],
2685 );
2686 if (count ($tmp = array_diff ($port['old_allowed'], $port['new_allowed'])))
2687 $crq[] = array
2688 (
2689 'opcode' => 'rem allowed',
2690 'port' => $port_name,
2691 'vlans' => $tmp,
2692 );
2693 break;
2694 case 'access->access':
2695 if ($port['old_native'] and $port['old_native'] != $port['new_native'])
2696 $crq[] = array
2697 (
2698 'opcode' => 'unset access',
2699 'arg1' => $port_name,
2700 'arg2' => $port['old_native'],
2701 );
2702 break;
2703 case 'access->trunk':
2704 $crq[] = array
2705 (
2706 'opcode' => 'unset access',
2707 'arg1' => $port_name,
2708 'arg2' => $port['old_native'],
2709 );
2710 break;
2711 case 'trunk->access':
2712 $crq[] = array
2713 (
2714 'opcode' => 'unset native',
2715 'arg1' => $port_name,
2716 'arg2' => $port['old_native'],
2717 );
2718 if (count ($port['old_allowed']))
2719 $crq[] = array
2720 (
2721 'opcode' => 'rem allowed',
2722 'port' => $port_name,
2723 'vlans' => $port['old_allowed'],
2724 );
2725 break;
2726 default:
2727 throw new InvalidArgException ('ports_to_do', '(hidden)', 'error in structure');
2728 }
2729 // Now it is safe to unconfigure VLANs, which still exist on device,
2730 // but are not present on the "new" list.
2731 // FIXME: put all IDs into one pseudo-command to make it easier
2732 // for translators to create/destroy VLANs in batches, where
2733 // target platform allows them to do.
2734 foreach (array_diff ($old_managed_vlans, $new_managed_vlans) as $vlan_id)
2735 $crq[] = array
2736 (
2737 'opcode' => 'destroy VLAN',
2738 'arg1' => $vlan_id,
2739 );
2740 // Configure VLANs, which must be present on the device, but are not yet.
2741 foreach (array_diff ($new_managed_vlans, $old_managed_vlans) as $vlan_id)
2742 $crq[] = array
2743 (
2744 'opcode' => 'create VLAN',
2745 'arg1' => $vlan_id,
2746 );
2747 // Now, when all new VLANs are created (queued), it is safe to assign (queue)
2748 // ports to the new VLANs.
2749 foreach ($ports_to_do as $port_name => $port)
2750 switch ($port['old_mode'] . '->' . $port['new_mode'])
2751 {
2752 case 'trunk->trunk':
2753 // For each allowed VLAN, which is present on the "new" list and missing from
2754 // the "old" one, queue a command to assign current port to that VLAN.
2755 if (count ($tmp = array_diff ($port['new_allowed'], $port['old_allowed'])))
2756 $crq[] = array
2757 (
2758 'opcode' => 'add allowed',
2759 'port' => $port_name,
2760 'vlans' => $tmp,
2761 );
2762 // One of the "allowed" VLANs for this port may probably be "native".
2763 // "new native" is set and differs from "old native"
2764 if ($port['new_native'] and $port['new_native'] != $port['old_native'])
2765 $crq[] = array
2766 (
2767 'opcode' => 'set native',
2768 'arg1' => $port_name,
2769 'arg2' => $port['new_native'],
2770 );
2771 break;
2772 case 'access->access':
2773 if ($port['new_native'] and $port['new_native'] != $port['old_native'])
2774 $crq[] = array
2775 (
2776 'opcode' => 'set access',
2777 'arg1' => $port_name,
2778 'arg2' => $port['new_native'],
2779 );
2780 break;
2781 case 'access->trunk':
2782 $crq[] = array
2783 (
2784 'opcode' => 'set mode',
2785 'arg1' => $port_name,
2786 'arg2' => $port['new_mode'],
2787 );
2788 if (count ($port['new_allowed']))
2789 $crq[] = array
2790 (
2791 'opcode' => 'add allowed',
2792 'port' => $port_name,
2793 'vlans' => $port['new_allowed'],
2794 );
2795 $crq[] = array
2796 (
2797 'opcode' => 'set native',
2798 'arg1' => $port_name,
2799 'arg2' => $port['new_native'],
2800 );
2801 break;
2802 case 'trunk->access':
2803 $crq[] = array
2804 (
2805 'opcode' => 'set mode',
2806 'arg1' => $port_name,
2807 'arg2' => $port['new_mode'],
2808 );
2809 $crq[] = array
2810 (
2811 'opcode' => 'set access',
2812 'arg1' => $port_name,
2813 'arg2' => $port['new_native'],
2814 );
2815 break;
2816 default:
2817 throw new InvalidArgException ('ports_to_do', '(hidden)', 'error in structure');
2818 }
2819 if (count ($crq))
2820 {
2821 array_unshift ($crq, array ('opcode' => 'begin configuration'));
2822 $crq[] = array ('opcode' => 'end configuration');
2823 if (considerConfiguredConstraint (spotEntity ('object', $vswitch['object_id']), '8021Q_WRI_AFTER_CONFT_LISTSRC'))
2824 $crq[] = array ('opcode' => 'save configuration');
2825 setDevice8021QConfig ($vswitch['object_id'], $crq);
2826 }
2827 return count ($crq);
2828 }
2829
2830 // filter list of changed ports to cancel changes forbidden by VST and domain
2831 function filter8021QChangeRequests
2832 (
2833 $domain_vlanlist,
2834 $before, // current saved configuration of all ports
2835 $changes // changed ports with VST markup
2836 )
2837 {
2838 $domain_immune_vlans = array();
2839 foreach ($domain_vlanlist as $vlan_id => $vlan)
2840 if ($vlan['vlan_type'] == 'alien')
2841 $domain_immune_vlans[] = $vlan_id;
2842 $ret = array();
2843 foreach ($changes as $port_name => $port)
2844 {
2845 // VST violation ?
2846 if (!goodModeForVSTRole ($port['mode'], $port['vst_role']))
2847 continue; // ignore change request
2848 // find and cancel any changes regarding immune VLANs
2849 switch ($port['mode'])
2850 {
2851 case 'access':
2852 foreach ($domain_immune_vlans as $immune)
2853 // Reverting an attempt to set an access port from
2854 // "normal" VLAN to immune one (or vice versa) requires
2855 // special handling, becase the calling function has
2856 // discarded the old contents of 'allowed' for current port.
2857 if
2858 (
2859 $before[$port_name]['native'] == $immune or
2860 $port['native'] == $immune
2861 )
2862 {
2863 $port['native'] = $before[$port_name]['native'];
2864 $port['allowed'] = array ($port['native']);
2865 // Such reversal happens either once or never for an
2866 // access port.
2867 break;
2868 }
2869 break;
2870 case 'trunk':
2871 foreach ($domain_immune_vlans as $immune)
2872 if (in_array ($immune, $before[$port_name]['allowed'])) // was allowed before
2873 {
2874 if (!in_array ($immune, $port['allowed']))
2875 $port['allowed'][] = $immune; // restore
2876 if ($before[$port_name]['native'] == $immune) // and was native
2877 $port['native'] = $immune; // also restore
2878 }
2879 else // wasn't
2880 {
2881 if (in_array ($immune, $port['allowed']))
2882 unset ($port['allowed'][array_search ($immune, $port['allowed'])]); // cancel
2883 if ($port['native'] == $immune)
2884 $port['native'] = $before[$port_name]['native'];
2885 }
2886 break;
2887 default:
2888 throw new InvalidArgException ('mode', $port['mode']);
2889 }
2890 // save work
2891 $ret[$port_name] = $port;
2892 }
2893 return $ret;
2894 }
2895
2896 // take port list with order applied and return uplink ports in the same format
2897 function produceUplinkPorts ($domain_vlanlist, $portlist)
2898 {
2899 $ret = array();
2900 $employed = array();
2901 foreach ($domain_vlanlist as $vlan_id => $vlan)
2902 if ($vlan['vlan_type'] == 'compulsory')
2903 $employed[] = $vlan_id;
2904 foreach ($portlist as $port_name => $port)
2905 if ($port['vst_role'] != 'uplink')
2906 foreach ($port['allowed'] as $vlan_id)
2907 if (!in_array ($vlan_id, $employed))
2908 $employed[] = $vlan_id;
2909 foreach ($portlist as $port_name => $port)
2910 if ($port['vst_role'] == 'uplink')
2911 {
2912 $employed_here = array();
2913 foreach ($employed as $vlan_id)
2914 if (matchVLANFilter ($vlan_id, $port['wrt_vlans']))
2915 $employed_here[] = $vlan_id;
2916 $ret[$port_name] = array
2917 (
2918 'vst_role' => 'uplink',
2919 'mode' => 'trunk',
2920 'allowed' => $employed_here,
2921 'native' => 0,
2922 );
2923 }
2924 return $ret;
2925 }
2926
2927 function same8021QConfigs ($a, $b)
2928 {
2929 return $a['mode'] == $b['mode'] &&
2930 array_values_same ($a['allowed'], $b['allowed']) &&
2931 $a['native'] == $b['native'];
2932 }
2933
2934 // Return TRUE, if the port can be edited by the user.
2935 function editable8021QPort ($port)
2936 {
2937 return in_array ($port['vst_role'], array ('trunk', 'access', 'anymode'));
2938 }
2939
2940 // Decide, whether the given 802.1Q port mode is permitted by
2941 // VST port role.
2942 function goodModeForVSTRole ($mode, $role)
2943 {
2944 switch ($mode)
2945 {
2946 case 'access':
2947 return in_array ($role, array ('access', 'anymode'));
2948 case 'trunk':
2949 return in_array ($role, array ('trunk', 'uplink', 'downlink', 'anymode'));
2950 default:
2951 throw new InvalidArgException ('mode', $mode);
2952 }
2953 }
2954
2955 /*
2956
2957 Relation between desired (D), cached (C) and running (R)
2958 copies of switch ports (P) list.
2959
2960 D C R
2961 +---+ +---+ +---+
2962 | P |-----| P |-? +--| P |
2963 +---+ +---+ / +---+
2964 | P |-----| P |--+ ?-| P |
2965 +---+ +---+ +---+
2966 | P |-----| P |-------| P |
2967 +---+ +---+ +---+
2968 | P |-----| P |--+ ?-| P |
2969 +---+ +---+ \ +---+
2970 | P |-----| P |--+ +--| P |
2971 +---+ +---+ \ +---+
2972 +--| P |
2973 +---+
2974 ?-| P |
2975 +---+
2976
2977 A modified local version of a port in "conflict" state ignores remote
2978 changes until remote change maintains its difference. Once both edits
2979 match, the local copy "locks" on the remote and starts tracking it.
2980
2981 v
2982 a "o" -- remOte version
2983 l "l" -- Local version
2984 u "b" -- Both versions
2985 e
2986
2987 ^
2988 | o b
2989 | o
2990 | l l l l l l b b
2991 | o o b
2992 | o b
2993 |
2994 | o
2995 |
2996 |
2997 0----------------------------------------------> time
2998
2999 */
3000 function get8021QSyncOptions
3001 (
3002 $vswitch,
3003 $D, // desired config
3004 $C, // cached config
3005 $R // running-config
3006 )
3007 {
3008 $default_port = array
3009 (
3010 'mode' => 'access',
3011 'allowed' => array (VLAN_DFL_ID),
3012 'native' => VLAN_DFL_ID,
3013 );
3014 $ret = array();
3015 $allports = array();
3016 foreach (array_unique (array_merge (array_keys ($C), array_keys ($R))) as $pn)
3017 $allports[$pn] = array();
3018 foreach (apply8021QOrder ($vswitch['template_id'], $allports) as $pn => $port)
3019 {
3020 // catch anomalies early
3021 if ($port['vst_role'] == 'none')
3022 {
3023 if ((!array_key_exists ($pn, $R) or $R[$pn]['mode'] == 'none') and !array_key_exists ($pn, $C))
3024 $ret[$pn] = array ('status' => 'none');
3025 else
3026 $ret[$pn] = array
3027 (
3028 'status' => 'martian_conflict',
3029 'left' => array_key_exists ($pn, $C) ? $C[$pn] : array ('mode' => 'none'),
3030 'right' => array_key_exists ($pn, $R) ? $R[$pn] : array ('mode' => 'none'),
3031 );
3032 continue;
3033 }
3034 elseif ((!array_key_exists ($pn, $R) or $R[$pn]['mode'] == 'none') and array_key_exists ($pn, $C))
3035 {
3036 $ret[$pn] = array
3037 (
3038 'status' => 'martian_conflict',
3039 'left' => array_key_exists ($pn, $C) ? $C[$pn] : array ('mode' => 'none'),
3040 'right' => array_key_exists ($pn, $R) ? $R[$pn] : array ('mode' => 'none'),
3041 );
3042 continue;
3043 }
3044 // (DC_): port missing from device
3045 if (!array_key_exists ($pn, $R))
3046 {
3047 $ret[$pn] = array ('left' => $D[$pn]);
3048 if (same8021QConfigs ($D[$pn], $default_port))
3049 $ret[$pn]['status'] = 'ok_to_delete';
3050 else
3051 {
3052 $ret[$pn]['status'] = 'delete_conflict';
3053 $ret[$pn]['lastseen'] = $C[$pn];
3054 }
3055 continue;
3056 }
3057 // (__R): port missing from DB
3058 if (!array_key_exists ($pn, $C))
3059 {
3060 // Allow importing any configuration, which passes basic
3061 // validation. If port mode doesn't match its VST role,
3062 // this will be handled later WRT each port.
3063 $ret[$pn] = array
3064 (
3065 'status' => acceptable8021QConfig ($R[$pn]) ? 'ok_to_add' : 'add_conflict',
3066 'right' => $R[$pn],
3067 );
3068 continue;
3069 }
3070 $D_eq_C = same8021QConfigs ($D[$pn], $C[$pn]);
3071 $C_eq_R = same8021QConfigs ($C[$pn], $R[$pn]);
3072 // (DCR), D = C = R: data in sync
3073 if ($D_eq_C and $C_eq_R) // implies D == R
3074 {
3075 $ret[$pn] = array
3076 (
3077 'status' => 'in_sync',
3078 'both' => $R[$pn],
3079 );
3080 continue;
3081 }
3082 // (DCR), D = C: no local edit in the way
3083 if ($D_eq_C)
3084 $ret[$pn] = array
3085 (
3086 'status' => 'ok_to_pull',
3087 'left' => $D[$pn],
3088 'right' => $R[$pn],
3089 );
3090 // (DCR), C = R: no remote edit in the way
3091 elseif ($C_eq_R)
3092 $ret[$pn] = array
3093 (
3094 'status' => 'ok_to_push',
3095 'left' => $D[$pn],
3096 'right' => $R[$pn],
3097 );
3098 // (DCR), D = R: end of version conflict, restore tracking
3099 elseif (same8021QConfigs ($D[$pn], $R[$pn]))
3100 $ret[$pn] = array
3101 (
3102 'status' => 'ok_to_merge',
3103 'both' => $R[$pn],
3104 );
3105 else // D != C, C != R, D != R: version conflict
3106 $ret[$pn] = array
3107 (
3108 'status' => editable8021QPort ($port) ?
3109 // In case the port is normally updated by user, let him
3110 // resolve the conflict. If the system manages this port,
3111 // arrange the data to let remote version go down.
3112 'merge_conflict' : 'ok_to_push_with_merge',
3113 'left' => $D[$pn],
3114 'right' => $R[$pn],
3115 );
3116 }
3117 return $ret;
3118 }
3119
3120 // return number of records updated successfully of FALSE, if a conflict was in the way
3121 function exec8021QDeploy ($object_id, $do_push)
3122 {
3123 global $dbxlink;
3124 $nsaved = $npushed = $nsaved_uplinks = 0;
3125 $dbxlink->beginTransaction();
3126 if (NULL === $vswitch = getVLANSwitchInfo ($object_id, 'FOR UPDATE'))
3127 throw new InvalidArgException ('object_id', $object_id, 'VLAN domain is not set for this object');
3128 $D = getStored8021QConfig ($vswitch['object_id'], 'desired');
3129 $C = getStored8021QConfig ($vswitch['object_id'], 'cached');
3130 try
3131 {
3132 $R = getRunning8021QConfig ($vswitch['object_id']);
3133 }
3134 catch (RTGatewayError $e)
3135 {
3136 usePreparedExecuteBlade
3137 (
3138 'UPDATE VLANSwitch SET last_errno=?, last_error_ts=NOW() WHERE object_id=?',
3139 array (E_8021Q_PULL_REMOTE_ERROR, $vswitch['object_id'])
3140 );
3141 $dbxlink->commit();
3142 return 0;
3143 }
3144 $conflict = FALSE;
3145 $ok_to_push = array();
3146 foreach (get8021QSyncOptions ($vswitch, $D, $C, $R['portdata']) as $pn => $port)
3147 {
3148 // always update cache with new data from switch
3149 switch ($port['status'])
3150 {
3151 case 'ok_to_merge':
3152 // FIXME: this can be logged
3153 upd8021QPort ('cached', $vswitch['object_id'], $pn, $port['both']);
3154 break;
3155 case 'ok_to_delete':
3156 $nsaved += del8021QPort ($vswitch['object_id'], $pn);
3157 break;
3158 case 'ok_to_add':
3159 $nsaved += add8021QPort ($vswitch['object_id'], $pn, $port['right']);
3160 break;
3161 case 'delete_conflict':
3162 case 'merge_conflict':
3163 case 'add_conflict':
3164 case 'martian_conflict':
3165 $conflict = TRUE;
3166 break;
3167 case 'ok_to_pull':
3168 // FIXME: this can be logged
3169 upd8021QPort ('desired', $vswitch['object_id'], $pn, $port['right']);
3170 upd8021QPort ('cached', $vswitch['object_id'], $pn, $port['right']);
3171 $nsaved++;
3172 break;
3173 case 'ok_to_push_with_merge':
3174 upd8021QPort ('cached', $vswitch['object_id'], $pn, $port['right']);
3175 // fall through
3176 case 'ok_to_push':
3177 $ok_to_push[$pn] = $port['left'];
3178 break;
3179 }
3180 }
3181 // redo uplinks unconditionally
3182 $domain_vlanlist = getDomainVLANs ($vswitch['domain_id']);
3183 $Dnew = apply8021QOrder ($vswitch['template_id'], getStored8021QConfig ($vswitch['object_id'], 'desired'));
3184 // Take new "desired" configuration and derive uplink port configuration
3185 // from it. Then cancel changes to immune VLANs and save resulting
3186 // changes (if any left).
3187 $new_uplinks = filter8021QChangeRequests ($domain_vlanlist, $Dnew, produceUplinkPorts ($domain_vlanlist, $Dnew));
3188 $nsaved_uplinks += replace8021QPorts ('desired', $vswitch['object_id'], $Dnew, $new_uplinks);
3189 if ($nsaved + $nsaved_uplinks)
3190 {
3191 // saved configuration has changed (either "user" ports have changed,
3192 // or uplinks, or both), so bump revision number up)
3193 usePreparedExecuteBlade
3194 (
3195 'UPDATE VLANSwitch SET mutex_rev=mutex_rev+1, last_change=NOW(), out_of_sync="yes" WHERE object_id=?',
3196 array ($vswitch['object_id'])
3197 );
3198 }
3199 if ($conflict)
3200 usePreparedExecuteBlade
3201 (
3202 'UPDATE VLANSwitch SET out_of_sync="yes", last_errno=?, last_error_ts=NOW() WHERE object_id=?',
3203 array (E_8021Q_VERSION_CONFLICT, $vswitch['object_id'])
3204 );
3205 else
3206 {
3207 usePreparedExecuteBlade
3208 (
3209 'UPDATE VLANSwitch SET last_errno=?, last_error_ts=NOW() WHERE object_id=?',
3210 array (E_8021Q_NOERROR, $vswitch['object_id'])
3211 );
3212 // Modified uplinks are very likely to differ from those in R-copy,
3213 // so don't mark device as clean, if this happened. This can cost
3214 // us an additional, empty round of sync, but at least out_of_sync
3215 // won't be mistakenly set to 'no'.
3216 // FIXME: A cleaner way of coupling pull and push operations would
3217 // be to split this function into two.
3218 if (!count ($ok_to_push) and !$nsaved_uplinks)
3219 usePreparedExecuteBlade
3220 (
3221 'UPDATE VLANSwitch SET out_of_sync="no" WHERE object_id=?',
3222 array ($vswitch['object_id'])
3223 );
3224 elseif ($do_push)
3225 {
3226 usePreparedExecuteBlade
3227 (
3228 'UPDATE VLANSwitch SET last_push_started=NOW() WHERE object_id=?',
3229 array ($vswitch['object_id'])
3230 );
3231 try
3232 {
3233 $npushed += exportSwitch8021QConfig ($vswitch, $R['vlanlist'], $R['portdata'], $ok_to_push);
3234 // update cache for ports deployed
3235 replace8021QPorts ('cached', $vswitch['object_id'], $R['portdata'], $ok_to_push);
3236 usePreparedExecuteBlade
3237 (
3238 'UPDATE VLANSwitch SET last_push_finished=NOW(), out_of_sync="no", last_errno=? WHERE object_id=?',
3239 array (E_8021Q_NOERROR, $vswitch['object_id'])
3240 );
3241 }
3242 catch (RTGatewayError $r)
3243 {
3244 usePreparedExecuteBlade
3245 (
3246 'UPDATE VLANSwitch SET out_of_sync="yes", last_error_ts=NOW(), last_errno=? WHERE object_id=?',
3247 array (E_8021Q_PUSH_REMOTE_ERROR, $vswitch['object_id'])
3248 );
3249 }
3250 }
3251 }
3252 $dbxlink->commit();
3253 // start downlink work only after unlocking current object to make deadlocks less likely to happen
3254 // TODO: only process changed uplink ports
3255 if ($nsaved_uplinks)
3256 initiateUplinksReverb ($vswitch['object_id'], $new_uplinks);
3257 return $nsaved + $npushed + $nsaved_uplinks;
3258 }
3259
3260 // print part of HTML HEAD block
3261 function printPageHeaders ()
3262 {
3263 global $pageheaders;
3264 ksort ($pageheaders);
3265 foreach ($pageheaders as $s)
3266 echo $s . "\n";
3267 echo "<style type='text/css'>\n";
3268 foreach (array ('F', 'A', 'U', 'T', 'Th', &apo