r4073 minor change in UI for recent change: VST form commit on ENTER and hints on...
[racktables] / inc / gateways.php
1 <?php
2 /*
3 *
4 * This file contains gateway functions for RackTables.
5 * A gateway is an external executable, which provides
6 * read-only or read-write access to some external entities.
7 * Each gateway accepts its own list of command-line args
8 * and then reads its stdin for requests. Each request consists
9 * of one line and results in exactly one line of reply.
10 * The replies must have the following syntax:
11 * OK<space>any text up to the end of the line
12 * ERR<space>any text up to the end of the line
13 *
14 */
15
16 // translating functions maps
17 $gwrxlator = array();
18 $gwrxlator['getcdpstatus'] = array
19 (
20 'ios12' => 'ios12ReadCDPStatus',
21 'nxos4' => 'ios12ReadCDPStatus',
22 );
23 $gwrxlator['getlldpstatus'] = array
24 (
25 'ios12' => 'ios12ReadLLDPStatus',
26 'xos12' => 'xos12ReadLLDPStatus',
27 'vrp53' => 'vrp53ReadLLDPStatus',
28 'vrp55' => 'vrp55ReadLLDPStatus',
29 );
30 $gwrxlator['get8021q'] = array
31 (
32 'ios12' => 'ios12ReadVLANConfig',
33 'fdry5' => 'fdry5ReadVLANConfig',
34 'vrp53' => 'vrp53ReadVLANConfig',
35 'vrp55' => 'vrp55Read8021QConfig',
36 'nxos4' => 'nxos4Read8021QConfig',
37 'xos12' => 'xos12Read8021QConfig',
38 );
39 $gwrxlator['getportstatus'] = array
40 (
41 'ios12' => 'ciscoReadInterfaceStatus',
42 'vrp53' => 'vrpReadInterfaceStatus',
43 'vrp55' => 'vrpReadInterfaceStatus',
44 'nxos4' => 'ciscoReadInterfaceStatus',
45 );
46 $gwrxlator['getmaclist'] = array
47 (
48 'ios12' => 'ios12ReadMacList',
49 'vrp53' => 'vrp53ReadMacList',
50 'vrp55' => 'vrp55ReadMacList',
51 'nxos4' => 'nxos4ReadMacList',
52 );
53
54 $gwrxlator['gethndp']['vrp53'] = 'vrp53ReadHNDPStatus';
55
56 $gwpushxlator = array
57 (
58 'ios12' => 'ios12TranslatePushQueue',
59 'fdry5' => 'fdry5TranslatePushQueue',
60 'vrp53' => 'vrp53TranslatePushQueue',
61 'vrp55' => 'vrp55TranslatePushQueue',
62 'nxos4' => 'ios12TranslatePushQueue', // employ syntax compatibility
63 'xos12' => 'xos12TranslatePushQueue',
64 );
65
66 // This function launches specified gateway with specified
67 // command-line arguments and feeds it with the commands stored
68 // in the second arg as array.
69 // The answers are stored in another array, which is returned
70 // by this function. In the case when a gateway cannot be found,
71 // finishes prematurely or exits with non-zero return code,
72 // a single-item array is returned with the only "ERR" record,
73 // which explains the reason.
74 function queryGateway ($gwname, $questions)
75 {
76 $execpath = "./gateways/{$gwname}/main";
77 $dspec = array
78 (
79 0 => array ("pipe", "r"),
80 1 => array ("pipe", "w"),
81 2 => array ("file", "/dev/null", "a")
82 );
83 $pipes = array();
84 $gateway = proc_open ($execpath, $dspec, $pipes);
85 if (!is_resource ($gateway))
86 return array ('ERR proc_open() failed in ' . __FUNCTION__);
87
88 // Dialogue starts. Send all questions.
89 foreach ($questions as $q)
90 fwrite ($pipes[0], "$q\n");
91 fclose ($pipes[0]);
92
93 // Fetch replies.
94 $answers = array ();
95 while (!feof($pipes[1]))
96 {
97 $a = fgets ($pipes[1]);
98 if (!strlen ($a))
99 continue;
100 // Somehow I got a space appended at the end. Kick it.
101 $answers[] = trim ($a);
102 }
103 fclose($pipes[1]);
104
105 $retval = proc_close ($gateway);
106 if ($retval != 0)
107 throw new RTGatewayError ("gateway failed with code ${retval}");
108 if (!count ($answers))
109 throw new RTGatewayError ('no response from gateway');
110 if (count ($answers) != count ($questions))
111 throw new RTGatewayError ('protocol violation');
112 foreach ($answers as $a)
113 if (strpos ($a, 'OK!') !== 0)
114 throw new RTGatewayError ("subcommand failed with status: ${a}");
115 return $answers;
116 }
117
118 // This functions returns an array for VLAN list, and an array for port list (both
119 // form another array themselves) and another one with MAC address list.
120 // The ports in the latter array are marked with either VLAN ID or 'trunk'.
121 // We don't sort the port list, as the gateway is believed to have done this already
122 // (or at least the underlying switch software ought to). This is important, as the
123 // port info is transferred to/from form not by names, but by numbers.
124 function getSwitchVLANs ($object_id = 0)
125 {
126 global $remote_username;
127 $objectInfo = spotEntity ('object', $object_id);
128 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
129 if (count ($endpoints) == 0)
130 throw new RTGatewayError ('no management address set');
131 if (count ($endpoints) > 1)
132 throw new RTGatewayError ('cannot pick management address');
133 $hwtype = $swtype = 'unknown';
134 foreach (getAttrValues ($object_id) as $record)
135 {
136 if ($record['name'] == 'SW type' && strlen ($record['o_value']))
137 $swtype = str_replace (' ', '+', execGMarker ($record['o_value']));
138 if ($record['name'] == 'HW type' && strlen ($record['o_value']))
139 $hwtype = str_replace (' ', '+', execGMarker ($record['o_value']));
140 }
141 $endpoint = str_replace (' ', '+', $endpoints[0]);
142 $commands = array
143 (
144 "connect ${endpoint} ${hwtype} ${swtype} ${remote_username}",
145 'listvlans',
146 'listports',
147 'listmacs'
148 );
149 $data = queryGateway ('switchvlans', $commands);
150 if (strpos ($data[0], 'OK!') !== 0)
151 throw new RTGatewayError ("gateway failed with status: ${data[0]}.");
152 // Now we have VLAN list in $data[1] and port list in $data[2]. Let's sort this out.
153 $tmp = array_unique (explode (';', substr ($data[1], strlen ('OK!'))));
154 if (count ($tmp) == 0)
155 throw new RTGatewayError ('gateway returned no records');
156 $vlanlist = array();
157 foreach ($tmp as $record)
158 {
159 list ($vlanid, $vlandescr) = explode ('=', $record);
160 $vlanlist[$vlanid] = $vlandescr;
161 }
162 $portlist = array();
163 foreach (explode (';', substr ($data[2], strlen ('OK!'))) as $pair)
164 {
165 list ($portname, $pair2) = explode ('=', $pair);
166 list ($status, $vlanid) = explode (',', $pair2);
167 $portlist[] = array ('portname' => $portname, 'status' => $status, 'vlanid' => $vlanid);
168 }
169 if (count ($portlist) == 0)
170 throw new RTGatewayError ('gateway returned no records');
171 $maclist = array();
172 foreach (explode (';', substr ($data[3], strlen ('OK!'))) as $pair)
173 {
174 list ($macaddr, $pair2) = explode ('=', $pair);
175 if (!strlen ($pair2))
176 continue;
177 list ($vlanid, $ifname) = explode ('@', $pair2);
178 $maclist[$ifname][$vlanid][] = $macaddr;
179 }
180 return array ($vlanlist, $portlist, $maclist);
181 }
182
183 function setSwitchVLANs ($object_id = 0, $setcmd)
184 {
185 global $remote_username;
186 if ($object_id <= 0)
187 return oneLiner (160); // invalid arguments
188 $objectInfo = spotEntity ('object', $object_id);
189 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
190 if (count ($endpoints) == 0)
191 return oneLiner (161); // endpoint not found
192 if (count ($endpoints) > 1)
193 return oneLiner (162); // can't pick an address
194 $hwtype = $swtype = 'unknown';
195 foreach (getAttrValues ($object_id) as $record)
196 {
197 if ($record['name'] == 'SW type' && strlen ($record['o_value']))
198 $swtype = strtr (execGMarker ($record['o_value']), ' ', '+');
199 if ($record['name'] == 'HW type' && strlen ($record['o_value']))
200 $hwtype = strtr (execGMarker ($record['o_value']), ' ', '+');
201 }
202 $endpoint = str_replace (' ', '+', $endpoints[0]);
203 $data = queryGateway
204 (
205 'switchvlans',
206 array ("connect ${endpoint} ${hwtype} ${swtype} ${remote_username}", $setcmd)
207 );
208 // Finally we can parse the response into message array.
209 $log_m = array();
210 foreach (explode (';', substr ($data[1], strlen ('OK!'))) as $text)
211 {
212 if (strpos ($text, 'C!') === 0)
213 {
214 $tmp = explode ('!', $text);
215 array_shift ($tmp);
216 $code = array_shift ($tmp);
217 $log_m[] = count ($tmp) ? array ('c' => $code, 'a' => $tmp) : array ('c' => $code); // gateway-encoded message
218 }
219 elseif (strpos ($text, 'I!') === 0)
220 $log_m[] = array ('c' => 62, 'a' => array (substr ($text, 2))); // generic gateway success
221 elseif (strpos ($text, 'W!') === 0)
222 $log_m[] = array ('c' => 202, 'a' => array (substr ($text, 2))); // generic gateway warning
223 else // All improperly formatted messages must be treated as error conditions.
224 $log_m[] = array ('c' => 166, 'a' => array (substr ($text, 2))); // generic gateway error
225 }
226 return $log_m;
227 }
228
229 // Drop a file off RackTables platform. The gateway will catch the file and pass it to the given
230 // installer script.
231 function gwSendFile ($endpoint, $handlername, $filetext = array())
232 {
233 global $remote_username;
234 $tmpnames = array();
235 $endpoint = str_replace (' ', '\ ', $endpoint); // the gateway dispatcher uses read (1) to assign arguments
236 $command = "submit ${remote_username} ${endpoint} ${handlername}";
237 foreach ($filetext as $text)
238 {
239 $name = tempnam ('', 'RackTables-sendfile-');
240 $tmpnames[] = $name;
241 if (FALSE === $name or FALSE === file_put_contents ($name, $text))
242 {
243 foreach ($tmpnames as $name)
244 unlink ($name);
245 throw new RTGatewayError ('failed to write to temporary file');
246 }
247 $command .= " ${name}";
248 }
249 try
250 {
251 queryGateway ('sendfile', array ($command));
252 foreach ($tmpnames as $name)
253 unlink ($name);
254 }
255 catch (RTGatewayError $e)
256 {
257 foreach ($tmpnames as $name)
258 unlink ($name);
259 throw $e;
260 }
261 }
262
263 // Query something through a gateway and get some text in return. Return that text.
264 function gwRecvFile ($endpoint, $handlername, &$output)
265 {
266 global $remote_username;
267 $tmpfilename = tempnam ('', 'RackTables-sendfile-');
268 $endpoint = str_replace (' ', '\ ', $endpoint); // the gateway dispatcher uses read (1) to assign arguments
269 try
270 {
271 queryGateway ('sendfile', array ("submit ${remote_username} ${endpoint} ${handlername} ${tmpfilename}"));
272 $output = file_get_contents ($tmpfilename);
273 unlink ($tmpfilename);
274 }
275 catch (RTGatewayError $e)
276 {
277 unlink ($tmpfilename);
278 throw $e;
279 }
280 if ($output === FALSE)
281 throw new RTGatewayError ('failed to read temporary file');
282 }
283
284 function gwSendFileToObject ($object_id = 0, $handlername, $filetext = '')
285 {
286 $objectInfo = spotEntity ('object', $object_id);
287 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
288 if (count ($endpoints) == 0)
289 throw new RTGatewayError ('no management address set');
290 if (count ($endpoints) > 1)
291 throw new RTGatewayError ('cannot pick management address');
292 gwSendFile (str_replace (' ', '+', $endpoints[0]), $handlername, array ($filetext));
293 }
294
295 function gwRecvFileFromObject ($object_id = 0, $handlername, &$output)
296 {
297 global $remote_username;
298 if ($object_id <= 0 or !strlen ($handlername))
299 return oneLiner (160); // invalid arguments
300 $objectInfo = spotEntity ('object', $object_id);
301 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
302 if (count ($endpoints) == 0)
303 return oneLiner (161); // endpoint not found
304 if (count ($endpoints) > 1)
305 return oneLiner (162); // can't pick an address
306 $endpoint = str_replace (' ', '+', $endpoints[0]);
307 gwRecvFile ($endpoint, $handlername, $output);
308 }
309
310 function detectDeviceBreed ($object_id)
311 {
312 $breed_by_swcode = array
313 (
314 251 => 'ios12',
315 252 => 'ios12',
316 254 => 'ios12',
317 963 => 'nxos4',
318 964 => 'nxos4',
319 1365 => 'nxos4',
320 1352 => 'xos12',
321 1360 => 'vrp53',
322 1361 => 'vrp55',
323 1369 => 'vrp55', // VRP versions 5.5 and 5.7 seem to be compatible
324 1363 => 'fdry5',
325 );
326 foreach (getAttrValues ($object_id) as $record)
327 if ($record['id'] == 4 and array_key_exists ($record['key'], $breed_by_swcode))
328 return $breed_by_swcode[$record['key']];
329 return '';
330 }
331
332 function getRunning8021QConfig ($object_id)
333 {
334 $ret = gwRetrieveDeviceConfig ($object_id, 'get8021q');
335 // Once there is no default VLAN in the parsed data, it means
336 // something else was parsed instead of config text.
337 if (!in_array (VLAN_DFL_ID, $ret['vlanlist']))
338 throw new RTGatewayError ('communication with device failed');
339 return $ret;
340 }
341
342 function setDevice8021QConfig ($object_id, $pseudocode)
343 {
344 if ('' == $breed = detectDeviceBreed ($object_id))
345 throw new RTGatewayError ('device breed unknown');
346 global $gwpushxlator;
347 // FIXME: this is a perfect place to log intended changes
348 gwDeployDeviceConfig ($object_id, $breed, unix2dos ($gwpushxlator[$breed] ($pseudocode)));
349 }
350
351 function gwRetrieveDeviceConfig ($object_id, $command)
352 {
353 global $gwrxlator;
354 if (!array_key_exists ($command, $gwrxlator))
355 throw new RTGatewayError ('command unknown');
356 $breed = detectDeviceBreed ($object_id);
357 if (!array_key_exists ($breed, $gwrxlator[$command]))
358 throw new RTGatewayError ('device breed unknown');
359 $objectInfo = spotEntity ('object', $object_id);
360 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
361 if (count ($endpoints) == 0)
362 throw new RTGatewayError ('no management address set');
363 if (count ($endpoints) > 1)
364 throw new RTGatewayError ('cannot pick management address');
365 $endpoint = str_replace (' ', '\ ', str_replace (' ', '+', $endpoints[0]));
366 $tmpfilename = tempnam ('', 'RackTables-deviceconfig-');
367 try
368 {
369 queryGateway ('deviceconfig', array ("${command} ${endpoint} ${breed} ${tmpfilename}"));
370 $configtext = file_get_contents ($tmpfilename);
371 unlink ($tmpfilename);
372 }
373 catch (RTGatewayError $e)
374 {
375 unlink ($tmpfilename);
376 throw $e;
377 }
378 if ($configtext === FALSE)
379 throw new RTGatewayError ('failed to read temporary file');
380 // Being here means it was alright.
381 return $gwrxlator[$command][$breed] (dos2unix ($configtext));
382 }
383
384 function gwDeployDeviceConfig ($object_id, $breed, $text)
385 {
386 if ($text == '')
387 throw new InvalidArgException ('text', '', 'deploy text is empty');
388 $objectInfo = spotEntity ('object', $object_id);
389 $endpoints = findAllEndpoints ($object_id, $objectInfo['name']);
390 if (count ($endpoints) == 0)
391 throw new RTGatewayError ('no management address set');
392 if (count ($endpoints) > 1)
393 throw new RTGatewayError ('cannot pick management address');
394 $endpoint = str_replace (' ', '\ ', str_replace (' ', '+', $endpoints[0]));
395 $tmpfilename = tempnam ('', 'RackTables-deviceconfig-');
396 if (FALSE === file_put_contents ($tmpfilename, $text))
397 {
398 unlink ($tmpfilename);
399 throw new RTGatewayError ('failed to write to temporary file');
400 }
401 try
402 {
403 queryGateway ('deviceconfig', array ("deploy ${endpoint} ${breed} ${tmpfilename}"));
404 unlink ($tmpfilename);
405 }
406 catch (RTGatewayError $e)
407 {
408 unlink ($tmpfilename);
409 throw $e;
410 }
411 }
412
413 // Read provided output of "show cdp neighbors detail" command and
414 // return a list of records with (translated) local port name,
415 // remote device name and (translated) remote port name.
416 function ios12ReadCDPStatus ($input)
417 {
418 $ret = array();
419 foreach (explode ("\n", $input) as $line)
420 {
421 $matches = array();
422 switch (TRUE)
423 {
424 case preg_match ('/^Device ID:\s*([A-Za-z0-9][A-Za-z0-9\.\-]*)/', $line, $matches):
425 case preg_match ('/^System Name:\s*([A-Za-z0-9][A-Za-z0-9\.\-]*)/', $line, $matches):
426 $ret['current']['device'] = $matches[1];
427 break;
428 case preg_match ('/^Interface: (.+), ?Port ID \(outgoing port\): (.+)$/', $line, $matches):
429 if (array_key_exists ('device', $ret['current']))
430 $ret[ios12ShortenIfName ($matches[1])][] = array
431 (
432 'device' => $ret['current']['device'],
433 'port' => ios12ShortenIfName ($matches[2]),
434 );
435 unset ($ret['current']);
436 break;
437 default:
438 }
439 }
440 unset ($ret['current']);
441 return $ret;
442 }
443
444 function ios12ReadLLDPStatus ($input)
445 {
446 $ret = array();
447 $got_header = FALSE;
448 foreach (explode ("\n", $input) as $line)
449 {
450 if (preg_match ("/^Device ID/", $line))
451 $got_header = TRUE;
452
453 if (!$got_header)
454 continue;
455
456 $matches = preg_split ('/\s+/', $line);
457
458 switch (count ($matches))
459 {
460 case 5:
461 list ($remote_name, $local_port, $ttl, $caps, $remote_port) = $matches;
462 $local_port = ios12ShortenIfName ($local_port);
463 $remote_port = ios12ShortenIfName ($remote_port);
464 $ret[$local_port][] = array
465 (
466 'device' => $remote_name,
467 'port' => $remote_port,
468 );
469 break;
470 default:
471 }
472 }
473 return $ret;
474 }
475
476 function xos12ReadLLDPStatus ($input)
477 {
478 $ret = array();
479 foreach (explode ("\n", $input) as $line)
480 {
481 $matches = array();
482 switch (TRUE)
483 {
484 case preg_match ('/^LLDP Port ([[:digit:]]+) detected \d+ neighbor$/', $line, $matches):
485 $ret['current']['local_port'] = ios12ShortenIfName ($matches[1]);
486 break;
487 case preg_match ('/^ Port ID : "(.+)"$/', $line, $matches):
488 $ret['current']['remote_port'] = ios12ShortenIfName ($matches[1]);
489 break;
490 case preg_match ('/^ - System Name: "(.+)"$/', $line, $matches):
491 if
492 (
493 array_key_exists ('current', $ret) and
494 array_key_exists ('local_port', $ret['current']) and
495 array_key_exists ('remote_port', $ret['current'])
496 )
497 $ret[$ret['current']['local_port']][] = array
498 (
499 'device' => $matches[1],
500 'port' => $ret['current']['remote_port'],
501 );
502 unset ($ret['current']);
503 default:
504 }
505 }
506 unset ($ret['current']);
507 return $ret;
508 }
509
510 function vrp53ReadLLDPStatus ($input)
511 {
512 $ret = array();
513 foreach (explode ("\n", $input) as $line)
514 {
515 $matches = array();
516 switch (TRUE)
517 {
518 case preg_match ('/^(.+) has \d+ neighbors:$/', $line, $matches):
519 $ret['current']['local_port'] = ios12ShortenIfName ($matches[1]);
520 break;
521 case preg_match ('/^(PortIdSubtype|PortId): ([^ ]+)/', $line, $matches):
522 $ret['current'][$matches[1]] = $matches[2];
523 break;
524 case preg_match ('/^SysName: (.+)$/', $line, $matches):
525 if
526 (
527 array_key_exists ('current', $ret) and
528 array_key_exists ('PortIdSubtype', $ret['current']) and
529 ($ret['current']['PortIdSubtype'] == 'interfaceAlias' or $ret['current']['PortIdSubtype'] == 'interfaceName') and
530 array_key_exists ('PortId', $ret['current']) and
531 array_key_exists ('local_port', $ret['current'])
532 )
533 $ret[$ret['current']['local_port']][] = array
534 (
535 'device' => $matches[1],
536 'port' => ios12ShortenIfName ($ret['current']['PortId']),
537 );
538 unset ($ret['current']);
539 break;
540 default:
541 }
542 }
543 unset ($ret['current']);
544 return $ret;
545 }
546
547 function vrp55ReadLLDPStatus ($input)
548 {
549 $ret = array();
550 foreach (explode ("\n", $input) as $line)
551 {
552 $matches = array();
553 switch (TRUE)
554 {
555 case preg_match ('/^(.+) has \d+ neighbors:$/', $line, $matches):
556 $ret['current']['local_port'] = ios12ShortenIfName ($matches[1]);
557 break;
558 case preg_match ('/^Port ID type :([^ ]+)/', $line, $matches):
559 $ret['current']['PortIdSubtype'] = $matches[1];
560 break;
561 case preg_match ('/^Port ID :(.+)$/', $line, $matches):
562 $ret['current']['PortId'] = $matches[1];
563 break;
564 case preg_match ('/^System name :(.+)$/', $line, $matches):
565 if
566 (
567 array_key_exists ('current', $ret) and
568 array_key_exists ('PortIdSubtype', $ret['current']) and
569 ($ret['current']['PortIdSubtype'] == 'interfaceAlias' or $ret['current']['PortIdSubtype'] == 'interfaceName') and
570 array_key_exists ('PortId', $ret['current']) and
571 array_key_exists ('local_port', $ret['current'])
572 )
573 $ret[$ret['current']['local_port']][] = array
574 (
575 'device' => $matches[1],
576 'port' => ios12ShortenIfName ($ret['current']['PortId']),
577 );
578 unset ($ret['current']);
579 break;
580 default:
581 }
582 }
583 unset ($ret['current']);
584 return $ret;
585 }
586
587 function vrp53ReadHNDPStatus ($input)
588 {
589 $ret = array();
590 foreach (explode ("\n", $input) as $line)
591 {
592 $matches = array();
593 switch (TRUE)
594 {
595 case preg_match ('/^ Interface: (.+)$/', $line, $matches):
596 $ret['current']['local_port'] = ios12ShortenIfName ($matches[1]);
597 break;
598 case preg_match ('/^ Port Name : (.+)$/', $line, $matches):
599 $ret['current']['remote_port'] = ios12ShortenIfName ($matches[1]);
600 break;
601 case preg_match ('/^ Device Name : (.+)$/', $line, $matches):
602 if
603 (
604 array_key_exists ('current', $ret) and
605 array_key_exists ('local_port', $ret['current']) and
606 array_key_exists ('remote_port', $ret['current'])
607 )
608 $ret[$ret['current']['local_port']][] = array
609 (
610 'device' => $matches[1],
611 'port' => $ret['current']['remote_port'],
612 );
613 unset ($ret['current']);
614 break;
615 default:
616 }
617 }
618 unset ($ret['current']);
619 return $ret;
620 }
621
622 function ios12ReadVLANConfig ($input)
623 {
624 $ret = array
625 (
626 'vlanlist' => array(),
627 'portdata' => array(),
628 );
629 $procfunc = 'ios12ScanTopLevel';
630 foreach (explode ("\n", $input) as $line)
631 $procfunc = $procfunc ($ret, $line);
632 return $ret;
633 }
634
635 function ios12ScanTopLevel (&$work, $line)
636 {
637 $matches = array();
638 switch (TRUE)
639 {
640 case (preg_match ('@^interface ((Ethernet|FastEthernet|GigabitEthernet|TenGigabitEthernet|Port-channel)[[:digit:]]+(/[[:digit:]]+)*)$@', $line, $matches)):
641 $work['current'] = array ('port_name' => ios12ShortenIfName ($matches[1]));
642 $work['current']['config'][] = array ('type' => 'line-header', 'line' => $line);
643 return 'ios12PickSwitchportCommand'; // switch to interface block reading
644 case (preg_match ('/^VLAN Name Status Ports$/', $line, $matches)):
645 return 'ios12PickVLANCommand';
646 default:
647 return __FUNCTION__; // continue scan
648 }
649 }
650
651 function ios12PickSwitchportCommand (&$work, $line)
652 {
653 if ($line[0] != ' ') // end of interface section
654 {
655 // save work, if it makes sense
656 switch ($work['current']['mode'])
657 {
658 case 'access':
659 if (!array_key_exists ('access vlan', $work['current']))
660 $work['current']['access vlan'] = 1;
661 $work['portdata'][$work['current']['port_name']] = array
662 (
663 'mode' => 'access',
664 'allowed' => array ($work['current']['access vlan']),
665 'native' => $work['current']['access vlan'],
666 );
667 break;
668 case 'trunk':
669 if (!array_key_exists ('trunk native vlan', $work['current']))
670 $work['current']['trunk native vlan'] = 1;
671 if (!array_key_exists ('trunk allowed vlan', $work['current']))
672 $work['current']['trunk allowed vlan'] = range (VLAN_MIN_ID, VLAN_MAX_ID);
673 // Having configured VLAN as "native" doesn't mean anything
674 // as long as it's not listed on the "allowed" line.
675 $effective_native = in_array
676 (
677 $work['current']['trunk native vlan'],
678 $work['current']['trunk allowed vlan']
679 ) ? $work['current']['trunk native vlan'] : 0;
680 $work['portdata'][$work['current']['port_name']] = array
681 (
682 'mode' => 'trunk',
683 'allowed' => $work['current']['trunk allowed vlan'],
684 'native' => $effective_native,
685 );
686 break;
687 case 'SKIP':
688 break;
689 case 'IP':
690 default:
691 // dot1q-tunnel, dynamic, private-vlan or even none --
692 // show in returned config and let user decide, if they
693 // want to fix device config or work around these ports
694 // by means of VST.
695 $work['portdata'][$work['current']['port_name']] = array
696 (
697 'mode' => 'none',
698 'allowed' => array(),
699 'native' => 0,
700 );
701 break;
702 }
703 if (isset ($work['portdata'][$work['current']['port_name']]))
704 $work['portdata'][$work['current']['port_name']]['config'] = $work['current']['config'];
705 unset ($work['current']);
706 return 'ios12ScanTopLevel';
707 }
708 // not yet
709 $matches = array();
710 $line_class = 'line-8021q';
711 switch (TRUE)
712 {
713 case (preg_match ('@^ switchport mode (.+)$@', $line, $matches)):
714 $work['current']['mode'] = $matches[1];
715 break;
716 case (preg_match ('@^ switchport access vlan (.+)$@', $line, $matches)):
717 $work['current']['access vlan'] = $matches[1];
718 break;
719 case (preg_match ('@^ switchport trunk native vlan (.+)$@', $line, $matches)):
720 $work['current']['trunk native vlan'] = $matches[1];
721 break;
722 case (preg_match ('@^ switchport trunk allowed vlan add (.+)$@', $line, $matches)):
723 $work['current']['trunk allowed vlan'] = array_merge
724 (
725 $work['current']['trunk allowed vlan'],
726 iosParseVLANString ($matches[1])
727 );
728 break;
729 case (preg_match ('@^ switchport trunk allowed vlan (.+)$@', $line, $matches)):
730 $work['current']['trunk allowed vlan'] = iosParseVLANString ($matches[1]);
731 break;
732 case preg_match ('@^ channel-group @', $line):
733 // port-channel subinterface config follows that of the master interface
734 $work['current']['mode'] = 'SKIP';
735 break;
736 case preg_match ('@^ ip address @', $line):
737 // L3 interface does no switchport functions
738 $work['current']['mode'] = 'IP';
739 break;
740 default: // suppress warning on irrelevant config clause
741 $line_class = 'line-other';
742 }
743 $work['current']['config'][] = array ('type' => $line_class, 'line' => $line);
744 return __FUNCTION__;
745 }
746
747 function ios12PickVLANCommand (&$work, $line)
748 {
749 $matches = array();
750 switch (TRUE)
751 {
752 case ($line == '---- -------------------------------- --------- -------------------------------'):
753 // ignore the rest of VLAN table header;
754 break;
755 case (preg_match ('@! END OF VLAN LIST$@', $line)):
756 return 'ios12ScanTopLevel';
757 case (preg_match ('@^([[:digit:]]+) {1,4}.{32} active @', $line, $matches)):
758 if (!array_key_exists ($matches[1], $work['vlanlist']))
759 $work['vlanlist'][] = $matches[1];
760 break;
761 default:
762 }
763 return __FUNCTION__;
764 }
765
766 // Another finite automata to read a dialect of Foundry configuration.
767 function fdry5ReadVLANConfig ($input)
768 {
769 $ret = array
770 (
771 'vlanlist' => array(),
772 'portdata' => array(),
773 );
774 $procfunc = 'fdry5ScanTopLevel';
775 foreach (explode ("\n", $input) as $line)
776 $procfunc = $procfunc ($ret, $line);
777 return $ret;
778 }
779
780 function fdry5ScanTopLevel (&$work, $line)
781 {
782 $matches = array();
783 switch (TRUE)
784 {
785 case (preg_match ('@^vlan ([[:digit:]]+)( name .+)? (by port)$@', $line, $matches)):
786 if (!array_key_exists ($matches[1], $work['vlanlist']))
787 $work['vlanlist'][] = $matches[1];
788 $work['current'] = array ('vlan_id' => $matches[1]);
789 return 'fdry5PickVLANSubcommand';
790 case (preg_match ('@^interface ethernet ([[:digit:]]+/[[:digit:]]+/[[:digit:]]+)$@', $line, $matches)):
791 $work['current'] = array ('port_name' => 'e' . $matches[1]);
792 return 'fdry5PickInterfaceSubcommand';
793 default:
794 return __FUNCTION__;
795 }
796 }
797
798 function fdry5PickVLANSubcommand (&$work, $line)
799 {
800 if ($line[0] != ' ') // end of VLAN section
801 {
802 unset ($work['current']);
803 return 'fdry5ScanTopLevel';
804 }
805 // not yet
806 $matches = array();
807 switch (TRUE)
808 {
809 case (preg_match ('@^ tagged (.+)$@', $line, $matches)):
810 // add current VLAN to 'allowed' list of each mentioned port
811 foreach (fdry5ParsePortString ($matches[1]) as $port_name)
812 if (array_key_exists ($port_name, $work['portdata']))
813 $work['portdata'][$port_name]['allowed'][] = $work['current']['vlan_id'];
814 else
815 $work['portdata'][$port_name] = array
816 (
817 'mode' => 'trunk',
818 'allowed' => array ($work['current']['vlan_id']),
819 'native' => 0, // can be updated later
820 );
821 $work['portdata'][$port_name]['mode'] = 'trunk';
822 break;
823 case (preg_match ('@^ untagged (.+)$@', $line, $matches)):
824 // replace 'native' column of each mentioned port with current VLAN ID
825 foreach (fdry5ParsePortString ($matches[1]) as $port_name)
826 {
827 if (array_key_exists ($port_name, $work['portdata']))
828 {
829 $work['portdata'][$port_name]['native'] = $work['current']['vlan_id'];
830 $work['portdata'][$port_name]['allowed'][] = $work['current']['vlan_id'];
831 }
832 else
833 $work['portdata'][$port_name] = array
834 (
835 'mode' => 'access',
836 'allowed' => array ($work['current']['vlan_id']),
837 'native' => $work['current']['vlan_id'],
838 );
839 // Untagged ports are initially assumed to be access ports, and
840 // when this assumption is right, this is the final port mode state.
841 // When the port is dual-mode one, this is detected and justified
842 // later in "interface" section of config text.
843 $work['portdata'][$port_name]['mode'] = 'access';
844 }
845 break;
846 default: // nom-nom
847 }
848 return __FUNCTION__;
849 }
850
851 function fdry5PickInterfaceSubcommand (&$work, $line)
852 {
853 if ($line[0] != ' ') // end of interface section
854 {
855 if (array_key_exists ('dual-mode', $work['current']))
856 {
857 if (array_key_exists ($work['current']['port_name'], $work['portdata']))
858 // update existing record
859 $work['portdata'][$work['current']['port_name']]['native'] = $work['current']['dual-mode'];
860 else
861 // add new
862 $work['portdata'][$work['current']['port_name']] = array
863 (
864 'allowed' => array ($work['current']['dual-mode']),
865 'native' => $work['current']['dual-mode'],
866 );
867 // a dual-mode port is always considered a trunk port
868 // (but not in the IronWare's meaning of "trunk") regardless of
869 // number of assigned tagged VLANs
870 $work['portdata'][$work['current']['port_name']]['mode'] = 'trunk';
871 }
872 unset ($work['current']);
873 return 'fdry5ScanTopLevel';
874 }
875 $matches = array();
876 switch (TRUE)
877 {
878 case (preg_match ('@^ dual-mode( +[[:digit:]]+ *)?$@', $line, $matches)):
879 // default VLAN ID for dual-mode command is 1
880 $work['current']['dual-mode'] = strlen (trim ($matches[1])) ? trim ($matches[1]) : 1;
881 break;
882 // FIXME: trunk/link-aggregate/ip address pulls port from 802.1Q field
883 default: // nom-nom
884 }
885 return __FUNCTION__;
886 }
887
888 function fdry5ParsePortString ($string)
889 {
890 $ret = array();
891 $tokens = explode (' ', trim ($string));
892 while (count ($tokens))
893 {
894 $letters = array_shift ($tokens); // "ethe", "to"
895 $numbers = array_shift ($tokens); // "x", "x/x", "x/x/x"
896 switch ($letters)
897 {
898 case 'ethe':
899 if ($prev_numbers != NULL)
900 $ret[] = 'e' . $prev_numbers;
901 $prev_numbers = $numbers;
902 break;
903 case 'to':
904 $ret = array_merge ($ret, fdry5GenPortRange ($prev_numbers, $numbers));
905 $prev_numbers = NULL; // no action on next token
906 break;
907 default: // ???
908 return array();
909 }
910 }
911 // flush delayed item
912 if ($prev_numbers != NULL)
913 $ret[] = 'e' . $prev_numbers;
914 return $ret;
915 }
916
917 // Take two indices in form "x", "x/x" or "x/x/x" and return the range of
918 // ports spanning from the first to the last. The switch software makes it
919 // easier to perform, because "ethe x/x/x to y/y/y" ranges never cross
920 // unit/slot boundary (every index except the last remains constant).
921 function fdry5GenPortRange ($from, $to)
922 {
923 $matches = array();
924 if (1 !== preg_match ('@^([[:digit:]]+/)?([[:digit:]]+/)?([[:digit:]]+)$@', $from, $matches))
925 return array();
926 $prefix = 'e' . $matches[1] . $matches[2];
927 $from_idx = $matches[3];
928 if (1 !== preg_match ('@^([[:digit:]]+/)?([[:digit:]]+/)?([[:digit:]]+)$@', $to, $matches))
929 return array();
930 $to_idx = $matches[3];
931 for ($i = $from_idx; $i <= $to_idx; $i++)
932 $ret[] = $prefix . $i;
933 return $ret;
934 }
935
936 // an implementation for Huawei syntax
937 function vrp53ReadVLANConfig ($input)
938 {
939 $ret = array
940 (
941 'vlanlist' => array(),
942 'portdata' => array(),
943 );
944 $procfunc = 'vrp53ScanTopLevel';
945 foreach (explode ("\n", $input) as $line)
946 $procfunc = $procfunc ($ret, $line);
947 return $ret;
948 }
949
950 function vrp53ScanTopLevel (&$work, $line)
951 {
952 $matches = array();
953 switch (TRUE)
954 {
955 case (preg_match ('@^ vlan batch (.+)$@', $line, $matches)):
956 foreach (vrp53ParseVLANString ($matches[1]) as $vlan_id)
957 $work['vlanlist'][] = $vlan_id;
958 return __FUNCTION__;
959 case (preg_match ('@^interface ((GigabitEthernet|XGigabitEthernet|Eth-Trunk)([[:digit:]]+(/[[:digit:]]+)*))$@', $line, $matches)):
960 $matches[1] = preg_replace ('@^GigabitEthernet(.+)$@', 'gi\\1', $matches[1]);
961 $matches[1] = preg_replace ('@^XGigabitEthernet(.+)$@', 'xg\\1', $matches[1]);
962 $matches[1] = preg_replace ('@^Eth-Trunk(.+)$@', 'et\\1', $matches[1]);
963 $work['current'] = array ('port_name' => $matches[1]);
964 $work['current']['config'][] = array ('type' => 'line-header', 'line' => $line);
965 return 'vrp53PickInterfaceSubcommand';
966 default:
967 return __FUNCTION__;
968 }
969 }
970
971 function vrp53ParseVLANString ($string)
972 {
973 $string = preg_replace ('/ to /', '-', $string);
974 $string = preg_replace ('/ /', ',', $string);
975 return iosParseVLANString ($string);
976 }
977
978 function vrp53PickInterfaceSubcommand (&$work, $line)
979 {
980 if ($line[0] == '#') // end of interface section
981 {
982 // Configuration Guide - Ethernet 3.3.4:
983 // "By default, the interface type is hybrid."
984 if (!array_key_exists ('link-type', $work['current']))
985 $work['current']['link-type'] = 'hybrid';
986 if (!array_key_exists ('allowed', $work['current']))
987 $work['current']['allowed'] = array();
988 if (!array_key_exists ('native', $work['current']))
989 $work['current']['native'] = 0;
990 switch ($work['current']['link-type'])
991 {
992 case 'access':
993 // VRP does not assign access ports to VLAN1 by default,
994 // leaving them blocked.
995 $work['portdata'][$work['current']['port_name']] =
996 $work['current']['native'] ? array
997 (
998 'allowed' => $work['current']['allowed'],
999 'native' => $work['current']['native'],
1000 'mode' => 'access',
1001 ) : array
1002 (
1003 'mode' => 'none',
1004 'allowed' => array(),
1005 'native' => 0,
1006 );
1007 break;
1008 case 'trunk':
1009 $work['portdata'][$work['current']['port_name']] = array
1010 (
1011 'allowed' => $work['current']['allowed'],
1012 'native' => 0,
1013 'mode' => 'trunk',
1014 );
1015 break;
1016 case 'hybrid':
1017 $work['portdata'][$work['current']['port_name']] = array
1018 (
1019 'allowed' => $work['current']['allowed'],
1020 'native' => $work['current']['native'],
1021 'mode' => 'trunk',
1022 );
1023 break;
1024 default: // dot1q-tunnel ?
1025 }
1026 if (isset ($work['portdata'][$work['current']['port_name']]))
1027 $work['portdata'][$work['current']['port_name']]['config'] = $work['current']['config'];
1028 unset ($work['current']);
1029 return 'vrp53ScanTopLevel';
1030 }
1031 $matches = array();
1032 $line_class = 'line-8021q';
1033 switch (TRUE)
1034 {
1035 case (preg_match ('@^ port default vlan ([[:digit:]]+)$@', $line, $matches)):
1036 $work['current']['native'] = $matches[1];
1037 if (!array_key_exists ('allowed', $work['current']))
1038 $work['current']['allowed'] = array();
1039 if (!in_array ($matches[1], $work['current']['allowed']))
1040 $work['current']['allowed'][] = $matches[1];
1041 break;
1042 case (preg_match ('@^ port link-type (.+)$@', $line, $matches)):
1043 $work['current']['link-type'] = $matches[1];
1044 break;
1045 case (preg_match ('@^ port trunk allow-pass vlan (.+)$@', $line, $matches)):
1046 if (!array_key_exists ('allowed', $work['current']))
1047 $work['current']['allowed'] = array();
1048 foreach (vrp53ParseVLANString ($matches[1]) as $vlan_id)
1049 if (!in_array ($vlan_id, $work['current']['allowed']))
1050 $work['current']['allowed'][] = $vlan_id;
1051 break;
1052 // TODO: make sure, that a port with "eth-trunk" clause always ends up in "none" mode
1053 default: // nom-nom
1054 $line_class = 'line-other';
1055 }
1056 $work['current']['config'][] = array('type' => $line_class, 'line' => $line);
1057 return __FUNCTION__;
1058 }
1059
1060 function vrp55Read8021QConfig ($input)
1061 {
1062 $ret = array
1063 (
1064 'vlanlist' => array (1), // VRP 5.50 hides VLAN1 from config text
1065 'portdata' => array(),
1066 );
1067 foreach (explode ("\n", $input) as $line)
1068 {
1069 $matches = array();
1070 // top level
1071 if (!array_key_exists ('current', $ret))
1072 {
1073 switch (TRUE)
1074 {
1075 case (preg_match ('@^ vlan batch (.+)$@', $line, $matches)):
1076 foreach (vrp53ParseVLANString ($matches[1]) as $vlan_id)
1077 $ret['vlanlist'][] = $vlan_id;
1078 break;
1079 case (preg_match ('@^interface ((GigabitEthernet|XGigabitEthernet|Eth-Trunk)([[:digit:]]+(/[[:digit:]]+)*))$@', $line, $matches)):
1080 $matches[1] = preg_replace ('@^GigabitEthernet(.+)$@', 'gi\\1', $matches[1]);
1081 $matches[1] = preg_replace ('@^XGigabitEthernet(.+)$@', 'xg\\1', $matches[1]);
1082 $ret['current'] = array ('port_name' => $matches[1]);
1083 $ret['current']['config'][] = array ('type' => 'line-header', 'line' => $line);
1084 break;
1085 }
1086 continue;
1087 }
1088 // inside an interface block
1089 $line_class = 'line-8021q';
1090 switch (TRUE)
1091 {
1092 case preg_match ('/^ port (link-type )?hybrid /', $line):
1093 throw new RTGatewayError ("unsupported configuration: ${line}");
1094 case preg_match ('/^ port link-type (.+)$/', $line, $matches):
1095 $ret['current']['link-type'] = $matches[1];
1096 break;
1097 // Native VLAN is configured differently for each link-type case, but
1098 // VRP is known to filter off clauses, which don't make sense for
1099 // current link-type. This way any interface section should contain
1100 // only one kind of "set native" clause (but if this constraint breaks,
1101 // we get a problem).
1102 case preg_match ('/^ port (default|trunk pvid) vlan ([[:digit:]]+)$/', $line, $matches):
1103 $ret['current']['native'] = $matches[2];
1104 if (!array_key_exists ('allowed', $ret['current']))
1105 $ret['current']['allowed'] = array();
1106 if (!in_array ($ret['current']['native'], $ret['current']['allowed']))
1107 $ret['current']['allowed'][] = $ret['current']['native'];
1108 break;
1109 case preg_match ('/^ port trunk allow-pass vlan (.+)$/', $line, $matches):
1110 if (!array_key_exists ('allowed', $ret['current']))
1111 $ret['current']['allowed'] = array();
1112 foreach (vrp53ParseVLANString ($matches[1]) as $vlan_id)
1113 if (!in_array ($vlan_id, $ret['current']['allowed']))
1114 $ret['current']['allowed'][] = $vlan_id;
1115 break;
1116 case $line == ' undo portswitch':
1117 case preg_match ('/^ ip address /', $line):
1118 $ret['current']['link-type'] = 'IP';
1119 break;
1120 case preg_match ('/^ eth-trunk /', $line):
1121 $ret['current']['link-type'] = 'SKIP';
1122 break;
1123 case substr ($line, 0, 1) == '#': // end of interface section
1124 if (!array_key_exists ('link-type', $ret['current']))
1125 throw new RTGatewayError ('unsupported configuration: link-type is neither trunk nor access for ' . $ret['current']['port_name']);
1126 if (!array_key_exists ('allowed', $ret['current']))
1127 $ret['current']['allowed'] = array();
1128 if (!array_key_exists ('native', $ret['current']))
1129 $ret['current']['native'] = 0;
1130 switch ($ret['current']['link-type'])
1131 {
1132 case 'access':
1133 // In VRP 5.50 an access port has default VLAN ID == 1
1134 $ret['portdata'][$ret['current']['port_name']] =
1135 $ret['current']['native'] ? array
1136 (
1137 'mode' => 'access',
1138 'allowed' => $ret['current']['allowed'],
1139 'native' => $ret['current']['native'],
1140 ) : array
1141 (
1142 'mode' => 'access',
1143 'allowed' => array (VLAN_DFL_ID),
1144 'native' => VLAN_DFL_ID,
1145 );
1146 break;
1147 case 'trunk':
1148 $ret['portdata'][$ret['current']['port_name']] = array
1149 (
1150 'mode' => 'trunk',
1151 'allowed' => $ret['current']['allowed'],
1152 'native' => $ret['current']['native'],
1153 );
1154 break;
1155 case 'IP':
1156 $ret['portdata'][$ret['current']['port_name']] = array
1157 (
1158 'mode' => 'none',
1159 'allowed' => array(),
1160 'native' => 0,
1161 );
1162 break;
1163 case 'SKIP':
1164 default: // dot1q-tunnel ?
1165 }
1166 if (isset ($ret['portdata'][$ret['current']['port_name']]))
1167 $ret['portdata'][$ret['current']['port_name']]['config'] = $ret['current']['config'];
1168 unset ($ret['current']);
1169 continue 2;
1170 default: // nom-nom
1171 $line_class = 'line-other';
1172 }
1173 $ret['current']['config'][] = array ('type' => $line_class, 'line' => $line);
1174 }
1175 return $ret;
1176 }
1177
1178 function nxos4Read8021QConfig ($input)
1179 {
1180 $ret = array
1181 (
1182 'vlanlist' => array(),
1183 'portdata' => array(),
1184 );
1185 $procfunc = 'nxos4ScanTopLevel';
1186 foreach (explode ("\n", $input) as $line)
1187 $procfunc = $procfunc ($ret, $line);
1188 return $ret;
1189 }
1190
1191 function nxos4ScanTopLevel (&$work, $line)
1192 {
1193 $matches = array();
1194 switch (TRUE)
1195 {
1196 case (preg_match ('@^interface ((Ethernet|Port-channel)[[:digit:]]+(/[[:digit:]]+)*)$@i', $line, $matches)):
1197 $matches[1] = preg_replace ('@^Ethernet(.+)$@i', 'e\\1', $matches[1]);
1198 $matches[1] = preg_replace ('@^Port-channel(.+)$@i', 'po\\1', $matches[1]);
1199 $work['current'] = array ('port_name' => $matches[1]);
1200 $work['current']['config'][] = array ('type' => 'line-header', 'line' => $line);
1201 return 'nxos4PickSwitchportCommand';
1202 case (preg_match ('@^vlan (.+)$@', $line, $matches)):
1203 foreach (iosParseVLANString ($matches[1]) as $vlan_id)
1204 $work['vlanlist'][] = $vlan_id;
1205 return __FUNCTION__;
1206 default:
1207 return __FUNCTION__; // continue scan
1208 }
1209 }
1210
1211 function nxos4PickSwitchportCommand (&$work, $line)
1212 {
1213 if ($line == '') // end of interface section
1214 {
1215 // fill in defaults
1216 if (!array_key_exists ('mode', $work['current']))
1217 $work['current']['mode'] = 'access';
1218 // save work, if it makes sense
1219 switch ($work['current']['mode'])
1220 {
1221 case 'access':
1222 if (!array_key_exists ('access vlan', $work['current']))
1223 $work['current']['access vlan'] = 1;
1224 $work['portdata'][$work['current']['port_name']] = array
1225 (
1226 'mode' => 'access',
1227 'allowed' => array ($work['current']['access vlan']),
1228 'native' => $work['current']['access vlan'],
1229 );
1230 break;
1231 case 'trunk':
1232 if (!array_key_exists ('trunk native vlan', $work['current']))
1233 $work['current']['trunk native vlan'] = 1;
1234 // FIXME: NX-OS reserves VLANs 3968 through 4047 plus 4094 for itself
1235 if (!array_key_exists ('trunk allowed vlan', $work['current']))
1236 $work['current']['trunk allowed vlan'] = range (VLAN_MIN_ID, VLAN_MAX_ID);
1237 // Having configured VLAN as "native" doesn't mean anything
1238 // as long as it's not listed on the "allowed" line.
1239 $effective_native = in_array
1240 (
1241 $work['current']['trunk native vlan'],
1242 $work['current']['trunk allowed vlan']
1243 ) ? $work['current']['trunk native vlan'] : 0;
1244 $work['portdata'][$work['current']['port_name']] = array
1245 (
1246 'mode' => 'trunk',
1247 'allowed' => $work['current']['trunk allowed vlan'],
1248 'native' => $effective_native,
1249 );
1250 break;
1251 case 'SKIP':
1252 case 'fex-fabric': // associated port-channel
1253 break;
1254 default:
1255 // dot1q-tunnel, dynamic, private-vlan
1256 $work['portdata'][$work['current']['port_name']] = array
1257 (
1258 'mode' => 'none',
1259 'allowed' => array(),
1260 'native' => 0,
1261 );
1262 // unset (routed), dot1q-tunnel, dynamic, private-vlan --- skip these
1263 }
1264 if (isset ($work['portdata'][$work['current']['port_name']]))
1265 $work['portdata'][$work['current']['port_name']]['config'] = $work['current']['config'];
1266 unset ($work['current']);
1267 return 'nxos4ScanTopLevel';
1268 }
1269 // not yet
1270 $matches = array();
1271 $line_class = 'line-8021q';
1272 switch (TRUE)
1273 {
1274 case (preg_match ('@^ switchport mode (.+)$@', $line, $matches)):
1275 $work['current']['mode'] = $matches[1];
1276 break;
1277 case (preg_match ('@^ switchport access vlan (.+)$@', $line, $matches)):
1278 $work['current']['access vlan'] = $matches[1];
1279 break;
1280 case (preg_match ('@^ switchport trunk native vlan (.+)$@', $line, $matches)):
1281 $work['current']['trunk native vlan'] = $matches[1];
1282 break;
1283 case (preg_match ('@^ switchport trunk allowed vlan add (.+)$@', $line, $matches)):
1284 $work['current']['trunk allowed vlan'] = array_merge
1285 (
1286 $work['current']['trunk allowed vlan'],
1287 iosParseVLANString ($matches[1])
1288 );
1289 break;
1290 case (preg_match ('@^ switchport trunk allowed vlan (.+)$@', $line, $matches)):
1291 $work['current']['trunk allowed vlan'] = iosParseVLANString ($matches[1]);
1292 break;
1293 case preg_match ('/^ +channel-group /', $line):
1294 $work['current']['mode'] = 'SKIP';
1295 break;
1296 default: // suppress warning on irrelevant config clause
1297 $line_class = 'line-other';
1298 }
1299 $work['current']['config'][] = array ('type' => $line_class, 'line' => $line);
1300 return __FUNCTION__;
1301 }
1302
1303 // Get a list of VLAN management pseudo-commands and return a text
1304 // of real vendor-specific commands, which implement the work.
1305 // This work is done in two rounds:
1306 // 1. For "add allowed" and "rem allowed" commands detect continuous
1307 // sequences of VLAN IDs and replace them with ranges of form "A-B",
1308 // where B>A.
1309 // 2. Iterate over the resulting list and produce real CLI commands.
1310 function ios12TranslatePushQueue ($queue)
1311 {
1312 $ret = '';
1313 foreach ($queue as $cmd)
1314 switch ($cmd['opcode'])
1315 {
1316 case 'create VLAN':
1317 $ret .= "vlan ${cmd['arg1']}\nexit\n";
1318 break;
1319 case 'destroy VLAN':
1320 $ret .= "no vlan ${cmd['arg1']}\n";
1321 break;
1322 case 'add allowed':
1323 case 'rem allowed':
1324 $clause = $cmd['opcode'] == 'add allowed' ? 'add' : 'remove';
1325 $ret .= "interface ${cmd['port']}\n";
1326 foreach (listToRanges ($cmd['vlans']) as $range)
1327 $ret .= "switchport trunk allowed vlan ${clause} " .
1328 ($range['from'] == $range['to'] ? $range['to'] : "${range['from']}-${range['to']}") .
1329 "\n";
1330 $ret .= "exit\n";
1331 break;
1332 case 'set native':
1333 $ret .= "interface ${cmd['arg1']}\nswitchport trunk native vlan ${cmd['arg2']}\nexit\n";
1334 break;
1335 case 'unset native':
1336 $ret .= "interface ${cmd['arg1']}\nno switchport trunk native vlan ${cmd['arg2']}\nexit\n";
1337 break;
1338 case 'set access':
1339 $ret .= "interface ${cmd['arg1']}\nswitchport access vlan ${cmd['arg2']}\nexit\n";
1340 break;
1341 case 'unset access':
1342 $ret .= "interface ${cmd['arg1']}\nno switchport access vlan\nexit\n";
1343 break;
1344 case 'set mode':
1345 $ret .= "interface ${cmd['arg1']}\nswitchport mode ${cmd['arg2']}\n";
1346 if ($cmd['arg2'] == 'trunk')
1347 $ret .= "no switchport trunk native vlan\nswitchport trunk allowed vlan none\n";
1348 $ret .= "exit\n";
1349 break;
1350 case 'begin configuration':
1351 $ret .= "configure terminal\n";
1352 break;
1353 case 'end configuration':
1354 $ret .= "end\n";
1355 break;
1356 case 'save configuration':
1357 $ret .= "copy running-config startup-config\n\n";
1358 break;
1359 default:
1360 throw new InvalidArgException ('opcode', $cmd['opcode']);
1361 }
1362 return $ret;
1363 }
1364
1365 function fdry5TranslatePushQueue ($queue)
1366 {
1367 $ret = '';
1368 foreach ($queue as $cmd)
1369 switch ($cmd['opcode'])
1370 {
1371 case 'create VLAN':
1372 $ret .= "vlan ${cmd['arg1']}\nexit\n";
1373 break;
1374 case 'destroy VLAN':
1375 $ret .= "no vlan ${cmd['arg1']}\n";
1376 break;
1377 case 'add allowed':
1378 foreach ($cmd['vlans'] as $vlan_id)
1379 $ret .= "vlan ${vlan_id}\ntagged ${cmd['port']}\nexit\n";
1380 break;
1381 case 'rem allowed':
1382 foreach ($cmd['vlans'] as $vlan_id)
1383 $ret .= "vlan ${vlan_id}\nno tagged ${cmd['port']}\nexit\n";
1384 break;
1385 case 'set native':
1386 $ret .= "interface ${cmd['arg1']}\ndual-mode ${cmd['arg2']}\nexit\n";
1387 break;
1388 case 'unset native':
1389 $ret .= "interface ${cmd['arg1']}\nno dual-mode ${cmd['arg2']}\nexit\n";
1390 break;
1391 case 'set access':
1392 $ret .= "vlan ${cmd['arg2']}\nuntagged ${cmd['arg1']}\nexit\n";
1393 break;
1394 case 'unset access':
1395 $ret .= "vlan ${cmd['arg2']}\nno untagged ${cmd['arg1']}\nexit\n";
1396 break;
1397 case 'set mode': // NOP
1398 break;
1399 case 'begin configuration':
1400 $ret .= "conf t\n";
1401 break;
1402 case 'end configuration':
1403 $ret .= "end\n";
1404 break;
1405 case 'save configuration':
1406 $ret .= "write memory\n";
1407 break;
1408 default:
1409 throw new InvalidArgException ('opcode', $cmd['opcode']);
1410 }
1411 return $ret;
1412 }
1413
1414 function vrp53TranslatePushQueue ($queue)
1415 {
1416 $ret = '';
1417 foreach ($queue as $cmd)
1418 switch ($cmd['opcode'])
1419 {
1420 case 'create VLAN':
1421 $ret .= "vlan ${cmd['arg1']}\nquit\n";
1422 break;
1423 case 'destroy VLAN':
1424 $ret .= "undo vlan ${cmd['arg1']}\n";
1425 break;
1426 case 'add allowed':
1427 case 'rem allowed':
1428 $clause = $cmd['opcode'] == 'add allowed' ? '' : 'undo ';
1429 $ret .= "interface ${cmd['port']}\n";
1430 foreach (listToRanges ($cmd['vlans']) as $range)
1431 $ret .= "${clause}port trunk allow-pass vlan " .
1432 ($range['from'] == $range['to'] ? $range['to'] : "${range['from']} to ${range['to']}") .
1433 "\n";
1434 $ret .= "quit\n";
1435 break;
1436 case 'set native':
1437 case 'set access':
1438 $ret .= "interface ${cmd['arg1']}\nport default vlan ${cmd['arg2']}\nquit\n";
1439 break;
1440 case 'unset native':
1441 case 'unset access':
1442 $ret .= "interface ${cmd['arg1']}\nundo port default vlan\nquit\n";
1443 break;
1444 case 'set mode':
1445 $modemap = array ('access' => 'access', 'trunk' => 'hybrid');
1446 $ret .= "interface ${cmd['arg1']}\nport link-type " . $modemap[$cmd['arg2']] . "\n";
1447 if ($cmd['arg2'] == 'hybrid')
1448 $ret .= "undo port default vlan\nundo port trunk allow-pass vlan all\n";
1449 $ret .= "quit\n";
1450 break;
1451 case 'begin configuration':
1452 $ret .= "system-view\n";
1453 break;
1454 case 'end configuration':
1455 $ret .= "return\n";
1456 break;
1457 case 'save configuration':
1458 $ret .= "save\nY\n";
1459 break;
1460 default:
1461 throw new InvalidArgException ('opcode', $cmd['opcode']);
1462 }
1463 return $ret;
1464 }
1465
1466 function vrp55TranslatePushQueue ($queue)
1467 {
1468 $ret = '';
1469 foreach ($queue as $cmd)
1470 switch ($cmd['opcode'])
1471 {
1472 case 'create VLAN':
1473 if ($cmd['arg1'] != 1)
1474 $ret .= "vlan ${cmd['arg1']}\nquit\n";
1475 break;
1476 case 'destroy VLAN':
1477 if ($cmd['arg1'] != 1)
1478 $ret .= "undo vlan ${cmd['arg1']}\n";
1479 break;
1480 case 'add allowed':
1481 case 'rem allowed':
1482 $undo = $cmd['opcode'] == 'add allowed' ? '' : 'undo ';
1483 $ret .= "interface ${cmd['port']}\n";
1484 foreach (listToRanges ($cmd['vlans']) as $range)
1485 $ret .= "${undo}port trunk allow-pass vlan " .
1486 ($range['from'] == $range['to'] ? $range['to'] : "${range['from']} to ${range['to']}") .
1487 "\n";
1488 $ret .= "quit\n";
1489 break;
1490 case 'set native':
1491 $ret .= "interface ${cmd['arg1']}\nport trunk pvid vlan ${cmd['arg2']}\nquit\n";
1492 break;
1493 case 'set access':
1494 $ret .= "interface ${cmd['arg1']}\nport default vlan ${cmd['arg2']}\nquit\n";
1495 break;
1496 case 'unset native':
1497 $ret .= "interface ${cmd['arg1']}\nundo port trunk pvid vlan\nquit\n";
1498 break;
1499 case 'unset access':
1500 $ret .= "interface ${cmd['arg1']}\nundo port default vlan\nquit\n";
1501 break;
1502 case 'set mode':
1503 // VRP 5.50's meaning of "trunk" is much like the one of IOS
1504 // (unlike the way VRP 5.30 defines "trunk" and "hybrid"),
1505 // but it is necessary to undo configured VLANs on a port
1506 // for mode change command to succeed.
1507 $undo = array
1508 (
1509 'access' => "undo port trunk allow-pass vlan all\n" .
1510 "port trunk allow-pass vlan 1\n" .
1511 "undo port trunk pvid vlan\n",
1512 'trunk' => "undo port default vlan\n",
1513 );
1514 $ret .= "interface ${cmd['arg1']}\n" . $undo[$cmd['arg2']];
1515 $ret .= "port link-type ${cmd['arg2']}\nquit\n";
1516 break;
1517 case 'begin configuration':
1518 $ret .= "system-view\n";
1519 break;
1520 case 'end configuration':
1521 $ret .= "return\n";
1522 break;
1523 case 'save configuration':
1524 $ret .= "save\nY\n";
1525 break;
1526 default:
1527 throw new InvalidArgException ('opcode', $cmd['opcode']);
1528 }
1529 return $ret;
1530 }
1531
1532 function xos12TranslatePushQueue ($queue)
1533 {
1534 $ret = '';
1535 foreach ($queue as $cmd)
1536 switch ($cmd['opcode'])
1537 {
1538 case 'create VLAN':
1539 $ret .= "create vlan VLAN${cmd['arg1']}\n";
1540 $ret .= "configure vlan VLAN${cmd['arg1']} tag ${cmd['arg1']}\n";
1541 break;
1542 case 'destroy VLAN':
1543 $ret .= "delete vlan VLAN${cmd['arg1']}\n";
1544 break;
1545 case 'add allowed':
1546 foreach ($cmd['vlans'] as $vlan_id)
1547 {
1548 $vlan_name = $vlan_id == 1 ? 'Default' : "VLAN${vlan_id}";
1549 $ret .= "configure vlan ${vlan_name} add ports ${cmd['port']} tagged\n";
1550 }
1551 break;
1552 case 'rem allowed':
1553 foreach ($cmd['vlans'] as $vlan_id)
1554 {
1555 $vlan_name = $vlan_id == 1 ? 'Default' : "VLAN${vlan_id}";
1556 $ret .= "configure vlan ${vlan_name} delete ports ${cmd['port']}\n";
1557 }
1558 break;
1559 case 'set native':
1560 $vlan_name = $cmd['arg2'] == 1 ? 'Default' : "VLAN${cmd['arg2']}";
1561 $ret .= "configure vlan ${vlan_name} delete ports ${cmd['arg1']}\n";
1562 $ret .= "configure vlan ${vlan_name} add ports ${cmd['arg1']} untagged\n";
1563 break;
1564 case 'unset native':
1565 $vlan_name = $cmd['arg2'] == 1 ? 'Default' : "VLAN${cmd['arg2']}";
1566 $ret .= "configure vlan ${vlan_name} delete ports ${cmd['arg1']}\n";
1567 $ret .= "configure vlan ${vlan_name} add ports ${cmd['arg1']} tagged\n";
1568 break;
1569 case 'set access':
1570 $vlan_name = $cmd['arg2'] == 1 ? 'Default' : "VLAN${cmd['arg2']}";
1571 $ret .= "configure vlan ${vlan_name} add ports ${cmd['arg1']} untagged\n";
1572 break;
1573 case 'unset access':
1574 $vlan_name = $cmd['arg2'] == 1 ? 'Default' : "VLAN${cmd['arg2']}";
1575 $ret .= "configure vlan ${vlan_name} delete ports ${cmd['arg1']}\n";
1576 break;
1577 case 'set mode':
1578 case 'begin configuration':
1579 case 'end configuration':
1580 break; // NOP
1581 case 'save configuration':
1582 $ret .= "save configuration\ny\n";
1583 break;
1584 default:
1585 throw new InvalidArgException ('opcode', $cmd['opcode']);
1586 }
1587 return $ret;
1588 }
1589
1590 function xos12Read8021QConfig ($input)
1591 {
1592 $ret = array
1593 (
1594 'vlanlist' => array (1),
1595 'portdata' => array(),
1596 );
1597 foreach (explode ("\n", $input) as $line)
1598 {
1599 $matches = array();
1600 switch (TRUE)
1601 {
1602 case (preg_match ('/^create vlan "([[:alnum:]]+)"$/', $line, $matches)):
1603 if (!preg_match ('/^VLAN[[:digit:]]+$/', $matches[1]))
1604 throw new RTGatewayError ('unsupported VLAN name ' . $matches[1]);
1605 break;
1606 case (preg_match ('/^configure vlan ([[:alnum:]]+) tag ([[:digit:]]+)$/', $line, $matches)):
1607 if (strtolower ($matches[1]) == 'default')
1608 throw new RTGatewayError ('default VLAN tag must be 1');
1609 if ($matches[1] != 'VLAN' . $matches[2])
1610 throw new RTGatewayError ("VLAN name ${matches[1]} does not match its tag ${matches[2]}");
1611 $ret['vlanlist'][] = $matches[2];
1612 break;
1613 case (preg_match ('/^configure vlan ([[:alnum:]]+) add ports (.+) (tagged|untagged) */', $line, $matches)):
1614 $submatch = array();
1615 if ($matches[1] == 'Default')
1616 $matches[1] = 'VLAN1';
1617 if (!preg_match ('/^VLAN([[:digit:]]+)$/', $matches[1], $submatch))
1618 throw new RTGatewayError ('unsupported VLAN name ' . $matches[1]);
1619 $vlan_id = $submatch[1];
1620 foreach (iosParseVLANString ($matches[2]) as $port_name)
1621 {
1622 if (!array_key_exists ($port_name, $ret['portdata']))
1623 $ret['portdata'][$port_name] = array
1624 (
1625 'mode' => 'trunk',
1626 'allowed' => array(),
1627 'native' => 0,
1628 );
1629 $ret['portdata'][$port_name]['allowed'][] = $vlan_id;
1630 if ($matches[3] == 'untagged')
1631 $ret['portdata'][$port_name]['native'] = $vlan_id;
1632 }
1633 break;
1634 default:
1635 }
1636 }
1637 return $ret;
1638 }
1639
1640 function ciscoReadInterfaceStatus ($text)
1641 {
1642 $result = array();
1643 $state = 'headerSearch';
1644 foreach (explode ("\n", $text) as $line)
1645 {
1646 switch ($state)
1647 {
1648 case 'headerSearch':
1649 if (preg_match('/^Port\s+Name\s+Status/', $line))
1650 {
1651 $name_field_borders = getColumnCoordinates($line, 'Name');
1652 if (isset ($name_field_borders['from']))
1653 $state = 'readPort';
1654 }
1655 break;
1656 case 'readPort':
1657 $portname = ios12ShortenIfName (trim (substr ($line, 0, $name_field_borders['from'])));
1658 $rest = trim (substr ($line, $name_field_borders['from'] + $name_field_borders['length'] + 1));
1659 $field_list = preg_split('/\s+/', $rest);
1660 if (count ($field_list) < 4)
1661 break;
1662 list ($status_raw, $vlan, $duplex, $speed, $type) = $field_list;
1663 if ($status_raw == 'connected' || $status_raw == 'up')
1664 $status = 'up';
1665 elseif ($status_raw == 'notconnect' || $status_raw == 'down')
1666 $status = 'down';
1667 else
1668 $status = 'disabled';
1669 $result[$portname] = array
1670 (
1671 'status' => $status,
1672 'speed' => $speed,
1673 'duplex' => $duplex,
1674 );
1675 break;
1676 }
1677 }
1678 return $result;
1679 }
1680
1681 function vrpReadInterfaceStatus ($text)
1682 {
1683 $result = array();
1684 $state = 'headerSearch';
1685 foreach (explode ("\n", $text) as $line)
1686 {
1687 switch ($state)
1688 {
1689 case 'headerSearch':
1690 if (preg_match('/^Interface\s+Phy\w*\s+Protocol/i', $line))
1691 $state = 'readPort';
1692 break;
1693 case 'readPort':
1694 if (preg_match('/[\$><\]]/', $line))
1695 break 2;
1696 $field_list = preg_split('/\s+/', $line);
1697 if (count ($field_list) < 7)
1698 break;
1699 list ($portname, $status_raw) = $field_list;
1700 $portname = ios12ShortenIfName ($portname);
1701
1702 if ($status_raw == 'up' || $status_raw == 'down')
1703 $status = $status_raw;
1704 else
1705 $status = 'disabled';
1706 $result[$portname] = array
1707 (
1708 'status' => $status,
1709 );
1710 break;
1711 }
1712 }
1713 return $result;
1714 }
1715
1716 function maclist_sort ($a, $b)
1717 {
1718 if ($a['vid'] == $b['vid'])
1719 return 0;
1720 return ($a['vid'] < $b['vid']) ? -1 : 1;
1721 }
1722
1723 function ios12ReadMacList ($text)
1724 {
1725 $result = array();
1726 $state = 'headerSearch';
1727 foreach (explode ("\n", $text) as $line)
1728 {
1729 switch ($state)
1730 {
1731 case 'headerSearch':
1732 if (preg_match('/Vlan\s+Mac Address\s+Type.*Ports?\s*$/i', $line))
1733 $state = 'readPort';
1734 break;
1735 case 'readPort':
1736 if (! preg_match ('/(\d+)\s+([a-f0-9]{4}\.[a-f0-9]{4}\.[a-f0-9]{4})\s.*?(\S+)$/', trim ($line), $matches))
1737 break;
1738 $portname = ios12ShortenIfName ($matches[3]);
1739 $result[$portname][] = array
1740 (
1741 'mac' => $matches[2],
1742 'vid' => $matches[1],
1743 );
1744 break;
1745 }
1746 }
1747 foreach ($result as $portname => &$maclist)
1748 usort ($maclist, 'maclist_sort');
1749 return $result;
1750 }
1751
1752 function nxos4ReadMacList ($text)
1753 {
1754 $result = array();
1755 $state = 'headerSearch';
1756 foreach (explode ("\n", $text) as $line)
1757 {
1758 switch ($state)
1759 {
1760 case 'headerSearch':
1761 if (preg_match('/VLAN\s+MAC Address\s+Type\s+age\s+Secure\s+NTFY\s+Ports/i', $line))
1762 $state = 'readPort';
1763 break;
1764 case 'readPort':
1765 if (! preg_match ('/(\d+)\s+([a-f0-9]{4}\.[a-f0-9]{4}\.[a-f0-9]{4})\s.*?(\S+)$/', trim ($line), $matches))
1766 break;
1767 $portname = ios12ShortenIfName ($matches[3]);
1768 $result[$portname][] = array
1769 (
1770 'mac' => $matches[2],
1771 'vid' => $matches[1],
1772 );
1773 break;
1774 }
1775 }
1776 foreach ($result as $portname => &$maclist)
1777 usort ($maclist, 'maclist_sort');
1778 return $result;
1779 }
1780
1781 function vrp53ReadMacList ($text)
1782 {
1783 $result = array();
1784 $state = 'headerSearch';
1785 foreach (explode ("\n", $text) as $line)
1786 {
1787 switch ($state)
1788 {
1789 case 'headerSearch':
1790 if (preg_match('/MAC Address\s+VLAN\/VSI\s+Port/i', $line))
1791 $state = 'readPort';
1792 break;
1793 case 'readPort':
1794 if (! preg_match ('/([a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4})\s+(\d+)\s+(\S+)/', trim ($line), $matches))
1795 break;
1796 $portname = ios12ShortenIfName ($matches[3]);
1797 $result[$portname][] = array
1798 (
1799 'mac' => str_replace ('-', '.', $matches[1]),
1800 'vid' => $matches[2],
1801 );
1802 break;
1803 }
1804 }
1805 foreach ($result as $portname => &$maclist)
1806 usort ($maclist, 'maclist_sort');
1807 return $result;
1808 }
1809
1810 function vrp55ReadMacList ($text)
1811 {
1812 $result = array();
1813 $state = 'headerSearch';
1814 foreach (explode ("\n", $text) as $line)
1815 {
1816 switch ($state)
1817 {
1818 case 'headerSearch':
1819 if (preg_match('/MAC Address\s+VLAN\/\S*\s+PEVLAN\s+CEVLAN\s+Port/i', $line))
1820 $state = 'readPort';
1821 break;
1822 case 'readPort':
1823 if (! preg_match ('/([a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4})\s+(\d+)(?:\s+\S+){2}\s+(\S+)/', trim ($line), $matches))
1824 break;
1825 $portname = ios12ShortenIfName ($matches[3]);
1826 $result[$portname][] = array
1827 (
1828 'mac' => str_replace ('-', '.', $matches[1]),
1829 'vid' => $matches[2],
1830 );
1831 break;
1832 }
1833 }
1834 foreach ($result as $portname => &$maclist)
1835 usort ($maclist, 'maclist_sort');
1836 return $result;
1837 }
1838
1839 ?>